♻️ refactor(pmowebrenderer): factoriser handlers et supprimer duplications

- Remplacer les Arc+Box::pin manuels par la macro action_handler!(captures(...))
- Supprimer helpers locaux dupliqués dans renderer.rs (add_var, add_action)
- Extraire handlers génériques pour GET requests
- Factoriser extraction metadata dans set_uri_handler /set_next_uriHandler  
- Simplifier build_renderingcontrol en retirant pipeline inutile
- Mettre à jour edition Rust de 2021 vers 2024 dans tous les Cargo.toml
- Corriger pattern matching inutile `ref` sur déréférencement dans pmoaudio et pmoflac
This commit is contained in:
2026-04-05 14:06:32 +02:00
parent 512dcd1803
commit fe9dd9aba6
21 changed files with 313 additions and 400 deletions

View File

@@ -0,0 +1,108 @@
# Plan: Suppression des duplications dans `@pmowebrenderer`
## Objectif
Éliminer les redondances de code identifiées lors de l'audit.
---
## Duplication 1: Helper functions dupliquées dans `renderer.rs`
**Fichiers affectés**: `src/renderer.rs`
**Problème**:
- Lignes 17-54: `add_arg_in`, `add_arg_out`, `add_var`, `add_action` définies
- Lignes 150-181: Dans `build_avtransport()`, réimplémentation locale avec closures `|svc, var| { ... }`
- Répétition de 20+ appels `add_var(&mut svc, &VAR)?` et `add_action(&mut svc, Arc::new(action))?`
**Solution**:
1. Supprimer les closures locales redéclarées (lignes 150-181)
2. Utiliser directement les fonctions helpers du haut du fichier
3. Créer une macro ou fonction utilitaire pour les appels répétés:
```rust
macro_rules! add_vars {
($svc:expr, $($var:expr),*) => { $({ add_var($svc, &$var)?; })* };
}
```
---
## Duplication 2: Pattern handlers boilerplate dans `handlers.rs`
**Fichiers affectés**: `src/handlers.rs`
**Problème**:
- `play_handler`, `stop_handler`, `pause_handler` (lignes 23-71): structure identique
- `next_handler`, `previous_handler` (lignes 74-95): clones
- Handlers GET (lignes 166-317): pattern `state.clone()` + `Box::pin(async move { ... set!() ... })` dupliqué
**Solution**:
1. Créer un helper générique:
```rust
fn make_state_handler<F>(state: SharedState, f: F) -> ActionHandler
where F: FnOnce(&mut ActionData, &RendererState) -> Result<ActionData, ActionError> + Send + 'static
```
2. Factoriser les closures `let state = state.clone()` dans chaque handler
---
## Duplication 3: Extraction metadata dupliquée
**Fichiers affectés**: `src/handlers.rs`
**Problème**:
- Lignes 118-123: `set_uri_handler` extraction metadata
- Lignes 146-151: `set_next_uri_handler` extraction metadata (identique)
**Solution**:
1. Extraire en fonction utilitaire:
```rust
fn extract_metadata(data: &ActionData, key: &str) -> String { ... }
```
---
## Duplication 4: Méthodes pipeline dans `registry.rs`
**Fichiers affectés**: `src/registry.rs`
**Problème**:
- `send_pipeline_command` (lignes 272-279) appelle `get_pipeline` (lignes 281-283)
- `load_uri` (lignes 286-290), `send_play_command` (lignes 293-296), `send_pause_command` (lignes 299-301) sont des wrappers quasi-identiques
**Solution**:
Consolider en méthodes génériques:
```rust
pub async fn send_command(&self, instance_id: &str, cmd: PipelineControl) {
if let Some(pipeline) = self.get_pipeline(instance_id) {
pipeline.send(cmd).await;
}
}
```
---
## Duplication 5: Feature flags avec code dupliqué
**Fichiers affectés**: `src/registry.rs`
**Problème**:
- Lignes 51-68 et 334-395: double impl de `create_instance` selon feature
**Solution**:
- Extraire la logique commune dans une fonction privée
- Utiliser `#[cfg]` seulement pour les différences (appel à pmoserver)
---
## Ordre de traitement suggéré
1. **Phase 1**: Helpers dans `renderer.rs` (les plus simples)
2. **Phase 2**: Handlers dans `handlers.rs` (plus complexe, nécessite macro)
3. **Phase 3**: Méthodes pipeline dans `registry.rs`
4. **Phase 4**: Feature flags
## Vérification
Après chaque phase, exécuter:
```bash
cargo check --package pmowebrenderer
```

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoapp"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
rust-embed = "8.5.0"

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoaudio-ext"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
# Core audio types

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoaudio"
version = "0.1.0"
edition = "2021"
edition = "2024"
[features]
default = []

View File

@@ -422,7 +422,7 @@ impl<'a> TryInto<&'a Arc<AudioChunk>> for &'a AudioSegment {
fn try_into(self) -> Result<&'a Arc<AudioChunk>, Self::Error> {
match &self.segment {
_AudioSegment::Chunk(ref chunk) => Ok(chunk),
_AudioSegment::Chunk(chunk) => Ok(chunk),
_ => Err(()),
}
}
@@ -433,7 +433,7 @@ impl<'a> TryInto<&'a Arc<SyncMarker>> for &'a AudioSegment {
fn try_into(self) -> Result<&'a Arc<SyncMarker>, Self::Error> {
match &self.segment {
_AudioSegment::Sync(ref marker) => Ok(marker),
_AudioSegment::Sync(marker) => Ok(marker),
_ => Err(()),
}
}

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoaudiocache"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
# Cache générique

View File

@@ -1,7 +1,7 @@
[package]
name = "pmocache"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
# Base de données

View File

@@ -2,7 +2,7 @@
[package]
name = "pmoconfig"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
pmoutils = { version = "0.1.2", registry = "pmo" }

View File

@@ -1,7 +1,7 @@
[package]
name = "pmocovers"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
# Cache générique

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoflac"
version = "0.1.0"
edition = "2021"
edition = "2024"
authors = ["PMOMusic"]
description = "Asynchronous FLAC <-> PCM streaming utilities"
license = "MIT"

View File

@@ -467,7 +467,7 @@ unsafe fn setup_metadata(
append_comment("TRACKNUMBER", &track_number.to_string())?;
}
// Construct cover URL: use cover_pk with server_base_url if available, fallback to cover_url
if let (Some(ref pk), Some(ref base_url)) = (&metadata.cover_pk, &metadata.server_base_url) {
if let (Some(pk), Some(base_url)) = (&metadata.cover_pk, &metadata.server_base_url) {
let cover_url = format!("{}/covers/image/{}", base_url, pk);
append_comment("COVERART", &cover_url)?;
} else if let Some(cover_url) = &metadata.cover_url {

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoparadise"
version = "0.1.0"
edition = "2021"
edition = "2024"
authors = ["PMOMusic Contributors"]
description = "Rust client for Radio Paradise streaming service"
license = "MIT OR Apache-2.0"

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoplaylist"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
# Caches PMO

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoqobuz"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
regex = "1.12"

View File

@@ -252,7 +252,7 @@ impl QobuzSource {
.await
.ok();
if let (Some(ref audio_pk), Some(ref cover_pk)) = (&cached_audio_pk, &cached_cover_pk) {
if let (Some(audio_pk), Some(cover_pk)) = (&cached_audio_pk, &cached_cover_pk) {
let _ =
self.inner
.cache_manager

View File

@@ -1,7 +1,7 @@
[package]
name = "pmoradiofrance"
version = "0.1.0"
edition = "2021"
edition = "2024"
authors = ["PMOMusic Contributors"]
description = "Rust client for Radio France streaming services"
license = "MIT OR Apache-2.0"

View File

@@ -1,7 +1,7 @@
[package]
name = "pmosource"
version = "0.1.0"
edition = "2021"
edition = "2024"
authors = ["PMOMusic Contributors"]
description = "Common traits and types for PMOMusic sources"
license = "MIT OR Apache-2.0"

View File

@@ -177,110 +177,27 @@ pub type ActionHandler = Arc<dyn Fn(ActionData) -> ActionFuture + Send + Sync>;
/// Macro pour créer facilement un ActionHandler.
///
/// Cette macro simplifie la création d'handlers asynchrones en cachant
/// la complexité de `Arc`, `Box::pin`, et `async move`.
/// Deux formes disponibles :
///
/// # Syntaxe
/// ## Forme simple (sans captures)
///
/// ```ignore
/// action_handler!(|data| {
/// // votre logique async avec ActionData
/// // Modifier les données et les retourner
/// Ok(data)
/// })
/// action_handler!(|data| { Ok(data) })
/// ```
///
/// # Arguments
/// ## Forme avec captures (clonées automatiquement à chaque appel)
///
/// - `data` : Paramètre de type [`ActionData`] - HashMap contenant les valeurs des arguments
/// - Le corps du bloc peut contenir du code asynchrone (`.await`)
///
/// # Type de retour
///
/// La macro retourne un [`ActionHandler`] prêt à l'emploi.
///
/// # Examples
///
/// ## Exemple 1 : Handler simple (retourne les données telles quelles)
/// Pour capturer un état partagé ou un handle, utilisez `captures(...)`.
/// Chaque variable listée est clonée une fois par invocation du handler,
/// ce qui satisfait la contrainte `Fn` (et non `FnOnce`).
///
/// ```ignore
/// use pmoupnp::action_handler;
///
/// let handler = action_handler!(|data| {
/// Ok(data) // Retourne les données non modifiées
/// });
/// ```
///
/// ## Exemple 2 : Handler qui calcule et modifie les données
///
/// ```ignore
/// use pmoupnp::{action_handler, get, set};
/// use pmoupnp::actions::ActionError;
///
/// let handler = action_handler!(|mut data| {
/// // Extraire les valeurs avec la macro get!
/// let celsius: f64 = get!(data, "Celsius", f64);
///
/// // Calculer
/// let fahrenheit = celsius * 9.0 / 5.0 + 32.0;
///
/// // Insérer avec la macro set!
/// set!(data, "Fahrenheit", fahrenheit);
///
/// Ok(data) // Retourner les données modifiées
/// });
/// ```
///
/// ## Exemple 3 : Handler avec logique métier asynchrone
///
/// ```ignore
/// use pmoupnp::{action_handler, get, set};
/// use pmoupnp::actions::ActionError;
///
/// let handler = action_handler!(|mut data| {
/// // Lire l'URI
/// let uri: String = get!(data, "URI", String);
///
/// // Appel asynchrone à un service externe
/// let metadata = external_service::fetch_metadata(&uri).await
/// .map_err(|e| ActionError::ExternalError(e.to_string()))?;
///
/// // Mettre à jour les données
/// set!(data, "Metadata", metadata);
///
/// Ok(data)
/// });
/// ```
///
/// ## Exemple 4 : Handler avec capture de contexte
///
/// ```ignore
/// use pmoupnp::{action_handler, get, set};
/// use pmoupnp::actions::ActionError;
/// use std::sync::Arc;
/// use tokio::sync::Mutex;
///
/// // Contexte partagé
/// let player_state = Arc::new(Mutex::new(PlayerState::Stopped));
///
/// let handler = action_handler!(|mut data| {
/// // Vérifier l'état
/// {
/// let state = player_state.lock().await;
/// if *state == PlayerState::Error {
/// return Err(ActionError::InvalidState("Player in error state".into()));
/// }
/// }
///
/// // Modifier l'état
/// {
/// let mut state = player_state.lock().await;
/// *state = PlayerState::Playing;
/// }
///
/// // Mettre à jour les données
/// set!(data, "TransportState", "PLAYING".to_string());
/// let state: SharedState = ...;
/// let pipeline: PipelineHandle = ...;
///
/// let handler = action_handler!(captures(state, pipeline) |mut data| {
/// pipeline.send(PipelineControl::Play).await;
/// state.write().playback_state = PlaybackState::Playing;
/// Ok(data)
/// });
/// ```
@@ -288,14 +205,41 @@ pub type ActionHandler = Arc<dyn Fn(ActionData) -> ActionFuture + Send + Sync>;
/// # Notes d'implémentation
///
/// - Le bloc est automatiquement wrappé dans `async move`
/// - Les captures de variables sont déplacées (`move`)
/// - Avec `captures(...)`, chaque variable capturée doit implémenter `Clone`
/// - Le résultat est automatiquement boxé et arcé
/// - Utilisez les macros `get!` et `set!` pour manipuler facilement les données
#[macro_export]
macro_rules! action_handler {
// ── Formes simples (sans captures externes) ──────────────────────────────
(|$data:ident| $body:block) => {
std::sync::Arc::new(|$data: $crate::actions::ActionData| {
Box::pin(async move $body)
})
};
(|mut $data:ident| $body:block) => {
std::sync::Arc::new(|mut $data: $crate::actions::ActionData| {
Box::pin(async move $body)
})
};
// ── Formes avec captures (clonées automatiquement à chaque appel) ────────
//
// Chaque variable listée dans captures(...) est clonée avant chaque appel,
// ce qui satisfait la contrainte `Fn` (vs `FnOnce`).
// Les variables capturées doivent implémenter `Clone + Send + Sync + 'static`.
(captures($($cap:ident),+ $(,)?) |$data:ident| $body:block) => {
std::sync::Arc::new(move |$data: $crate::actions::ActionData| {
$(let $cap = $cap.clone();)+
Box::pin(async move $body)
})
};
(captures($($cap:ident),+ $(,)?) |mut $data:ident| $body:block) => {
std::sync::Arc::new(move |mut $data: $crate::actions::ActionData| {
$(let $cap = $cap.clone();)+
Box::pin(async move $body)
})
};
}

View File

@@ -1,7 +1,7 @@
[package]
name = "pmowebrenderer"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
pmoupnp = { path = "../pmoupnp" }

View File

@@ -3,316 +3,197 @@
//! Chaque handler bridge une action UPnP vers une commande `PipelineControl`
//! envoyée au pipeline audio serveur, ou lit l'état partagé pour les requêtes GET.
use std::sync::Arc;
use pmodidl::DIDLLite;
use pmoupnp::actions::{ActionData, ActionError, ActionHandler, get_value};
use pmoupnp::{get, set};
use pmodidl::ToXmlElement;
use pmoupnp::{action_handler, get, set};
use pmoupnp::actions::{get_value, ActionHandler};
use crate::messages::PlaybackState;
use crate::pipeline::{PipelineControl, PipelineHandle, upnp_time_to_seconds};
use crate::pipeline::{upnp_time_to_seconds, PipelineControl, PipelineHandle};
use crate::state::SharedState;
type ActionFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<ActionData, ActionError>> + Send>>;
// ─── AVTransport : commandes de transport ─────────────────────────────────────
// ─── 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");
let has_uri = state.read().current_uri.is_some();
{
let mut s = state.write();
s.playback_state = PlaybackState::Transitioning;
}
if has_uri {
state.write().player_command = Some(serde_json::json!({
"type": "stream",
"url": "/api/webrenderer/stream"
}));
tracing::info!("UPnP Play: stored stream command for frontend polling");
}
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
action_handler!(captures(pipeline, state) |data| {
tracing::info!("[WebRenderer] UPnP Play action invoked");
let has_uri = state.read().current_uri.is_some();
state.write().playback_state = PlaybackState::Transitioning;
if has_uri {
state.write().player_command = Some(serde_json::json!({
"type": "stream",
"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();
let state = state.clone();
Box::pin(async move {
pipeline.send(PipelineControl::Stop).await;
state.write().playback_state = PlaybackState::Stopped;
Ok(data)
})
action_handler!(captures(pipeline, state) |data| {
pipeline.send(PipelineControl::Stop).await;
state.write().playback_state = PlaybackState::Stopped;
Ok(data)
})
}
/// 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();
let state = state.clone();
Box::pin(async move {
pipeline.send(PipelineControl::Pause).await;
state.write().playback_state = PlaybackState::Paused;
Ok(data)
})
action_handler!(captures(pipeline, state) |data| {
pipeline.send(PipelineControl::Pause).await;
state.write().playback_state = PlaybackState::Paused;
Ok(data)
})
}
/// 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 {
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
action_handler!(captures(pipeline) |data| {
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
}
/// 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 {
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
action_handler!(captures(pipeline) |data| {
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
}
/// 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();
Box::pin(async move {
let target: String = get!(&data, "Target", String);
let pos_sec = upnp_time_to_seconds(&target);
pipeline.send(PipelineControl::Seek(pos_sec)).await;
Ok(data)
})
action_handler!(captures(pipeline) |data| {
let target: String = get!(&data, "Target", String);
let pos_sec = upnp_time_to_seconds(&target);
pipeline.send(PipelineControl::Seek(pos_sec)).await;
Ok(data)
})
}
/// Handler pour l'action UPnP "SetAVTransportURI" - définit l'URI à jouer
// ─── AVTransport : chargement de média ────────────────────────────────────────
pub fn set_uri_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 SetAVTransportURI action invoked");
let uri: String = get!(&data, "CurrentURI", String);
let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
.or_else(|_| {
get_value::<DIDLLite>(&data, "CurrentURIMetaData")
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
action_handler!(captures(pipeline, state) |mut data| {
tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked");
let uri: String = get!(&data, "CurrentURI", String);
let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
.or_else(|_| get_value::<DIDLLite>(&data, "CurrentURIMetaData").map(|didl| didl.to_xml()))
.unwrap_or_default();
tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline");
pipeline.send(PipelineControl::LoadUri(uri.clone())).await;
tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline");
pipeline.send(PipelineControl::LoadUri(uri.clone())).await;
{
let mut s = state.write();
s.current_uri = Some(uri);
s.current_metadata = Some(metadata);
s.playback_state = PlaybackState::Transitioning;
}
Ok(data)
})
{
let mut s = state.write();
s.current_uri = Some(uri);
s.current_metadata = Some(metadata);
s.playback_state = PlaybackState::Transitioning;
}
Ok(data)
})
}
/// 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();
let state = state.clone();
Box::pin(async move {
let uri: String = get!(&data, "NextURI", String);
let metadata: String = get_value::<String>(&data, "NextURIMetaData")
.or_else(|_| {
get_value::<DIDLLite>(&data, "NextURIMetaData")
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
action_handler!(captures(pipeline, state) |mut data| {
let uri: String = get!(&data, "NextURI", String);
let metadata: String = get_value::<String>(&data, "NextURIMetaData")
.or_else(|_| get_value::<DIDLLite>(&data, "NextURIMetaData").map(|didl| didl.to_xml()))
.unwrap_or_default();
pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await;
{
let mut s = state.write();
s.next_uri = Some(uri);
s.next_metadata = Some(metadata);
}
Ok(data)
})
pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await;
{
let mut s = state.write();
s.next_uri = Some(uri);
s.next_metadata = Some(metadata);
}
Ok(data)
})
}
/// Handler pour l'action UPnP "GetPositionInfo" - retourne la position actuelle
// ─── AVTransport : getters ─────────────────────────────────────────────────────
pub fn get_position_info_handler(state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let state = state.clone();
Box::pin(async move {
let mut data = data;
let s = state.read();
set!(
&mut data,
"Track",
if s.current_uri.is_some() { 1u32 } else { 0u32 }
);
set!(
&mut data,
"TrackDuration",
s.duration.clone().unwrap_or_else(|| "00:00:00".to_string())
);
set!(
&mut data,
"TrackURI",
s.current_uri.clone().unwrap_or_default()
);
set!(
&mut data,
"TrackMetaData",
s.current_metadata.clone().unwrap_or_default()
);
set!(
&mut data,
"RelTime",
s.position.clone().unwrap_or_else(|| "00:00:00".to_string())
);
set!(
&mut data,
"AbsTime",
s.position.clone().unwrap_or_else(|| "00:00:00".to_string())
);
Ok(data)
})
action_handler!(captures(state) |mut data| {
let s = state.read();
set!(&mut data, "Track", if s.current_uri.is_some() { 1u32 } else { 0u32 });
set!(&mut data, "TrackDuration", s.duration.clone().unwrap_or_else(|| "00:00:00".to_string()));
set!(&mut data, "TrackURI", s.current_uri.clone().unwrap_or_default());
set!(&mut data, "TrackMetaData", s.current_metadata.clone().unwrap_or_default());
set!(&mut data, "RelTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string()));
set!(&mut data, "AbsTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string()));
Ok(data)
})
}
/// 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();
Box::pin(async move {
let mut data = data;
let s = state.read();
tracing::info!("[WebRenderer] GetTransportInfo: state={:?}", s.playback_state);
let transport_state = match s.playback_state {
PlaybackState::Stopped => "STOPPED",
PlaybackState::Playing => "PLAYING",
PlaybackState::Paused => "PAUSED_PLAYBACK",
PlaybackState::Transitioning => "TRANSITIONING",
};
set!(&mut data, "CurrentTransportState", transport_state.to_string());
set!(&mut data, "CurrentTransportStatus", "OK".to_string());
set!(&mut data, "CurrentSpeed", "1".to_string());
Ok(data)
})
action_handler!(captures(state) |mut data| {
let s = state.read();
tracing::info!("[WebRenderer] GetTransportInfo: state={:?}", s.playback_state);
let transport_state = match s.playback_state {
PlaybackState::Stopped => "STOPPED",
PlaybackState::Playing => "PLAYING",
PlaybackState::Paused => "PAUSED_PLAYBACK",
PlaybackState::Transitioning => "TRANSITIONING",
};
set!(&mut data, "CurrentTransportState", transport_state.to_string());
set!(&mut data, "CurrentTransportStatus", "OK".to_string());
set!(&mut data, "CurrentSpeed", "1".to_string());
Ok(data)
})
}
/// 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();
Box::pin(async move {
let mut data = data;
let s = state.read();
set!(
&mut data,
"NrTracks",
if s.current_uri.is_some() { 1u32 } else { 0u32 }
);
set!(&mut data, "CurrentURI", s.current_uri.clone().unwrap_or_default());
set!(&mut data, "CurrentURIMetaData", s.current_metadata.clone().unwrap_or_default());
set!(&mut data, "NextURI", s.next_uri.clone().unwrap_or_default());
set!(&mut data, "NextURIMetaData", s.next_metadata.clone().unwrap_or_default());
Ok(data)
})
action_handler!(captures(state) |mut data| {
let s = state.read();
set!(&mut data, "NrTracks", if s.current_uri.is_some() { 1u32 } else { 0u32 });
set!(&mut data, "CurrentURI", s.current_uri.clone().unwrap_or_default());
set!(&mut data, "CurrentURIMetaData", s.current_metadata.clone().unwrap_or_default());
set!(&mut data, "NextURI", s.next_uri.clone().unwrap_or_default());
set!(&mut data, "NextURIMetaData", s.next_metadata.clone().unwrap_or_default());
Ok(data)
})
}
// ─── RenderingControl Handlers ──────────────────────────────────────────────
// ─── ConnectionManager ─────────────────────────────────────────────────────────
/// 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();
Box::pin(async move {
let volume: u16 = get!(&data, "DesiredVolume", u16);
state.write().volume = volume;
Ok(data)
})
})
}
/// 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();
Box::pin(async move {
let mut data = data;
let volume = state.read().volume;
set!(&mut data, "CurrentVolume", volume);
Ok(data)
})
})
}
/// 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();
Box::pin(async move {
let mute: bool = get!(&data, "DesiredMute", bool);
state.write().mute = mute;
Ok(data)
})
})
}
/// 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();
Box::pin(async move {
let mut data = data;
let mute = state.read().mute;
set!(&mut data, "CurrentMute", mute);
Ok(data)
})
})
}
// ─── 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());
set!(
&mut data,
"Sink",
"http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string()
);
Ok(data)
})
action_handler!(|mut data| {
set!(&mut data, "Source", String::new());
set!(&mut data, "Sink", "http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string());
Ok(data)
})
}
// ─── RenderingControl ──────────────────────────────────────────────────────────
pub fn set_volume_handler(state: SharedState) -> ActionHandler {
action_handler!(captures(state) |mut data| {
let volume: u16 = get!(&data, "DesiredVolume", u16);
state.write().volume = volume;
Ok(data)
})
}
pub fn get_volume_handler(state: SharedState) -> ActionHandler {
action_handler!(captures(state) |mut data| {
let volume = state.read().volume;
set!(&mut data, "CurrentVolume", volume);
Ok(data)
})
}
pub fn set_mute_handler(state: SharedState) -> ActionHandler {
action_handler!(captures(state) |mut data| {
let mute: bool = get!(&data, "DesiredMute", bool);
state.write().mute = mute;
Ok(data)
})
}
pub fn get_mute_handler(state: SharedState) -> ActionHandler {
action_handler!(captures(state) |mut data| {
let mute = state.read().mute;
set!(&mut data, "CurrentMute", mute);
Ok(data)
})
}

View File

@@ -118,7 +118,7 @@ impl WebRendererFactory {
state: SharedState,
) -> Result<Device, FactoryError> {
let avtransport = Self::build_avtransport(pipeline.clone(), state.clone())?;
let renderingcontrol = Self::build_renderingcontrol(pipeline.clone(), state.clone())?;
let renderingcontrol = Self::build_renderingcontrol(state.clone())?;
let connectionmanager = Self::build_connectionmanager()?;
let short_name = extract_browser_name(browser_ua);
@@ -147,10 +147,6 @@ impl WebRendererFactory {
state: SharedState,
) -> Result<Service, FactoryError> {
let mut svc = Service::new("AVTransport".to_string());
let add_var = |svc: &mut Service, var: &Arc<pmoupnp::state_variables::StateVariable>| {
svc.add_variable(Arc::clone(var))
.map_err(|e| FactoryError::VariableError(e.to_string()))
};
// Ajouter toutes les variables d'état
add_var(&mut svc, &AVT_INSTANCE_ID)?;
@@ -176,11 +172,6 @@ impl WebRendererFactory {
add_var(&mut svc, &TRANSPORTSTATE)?;
add_var(&mut svc, &TRANSPORTSTATUS)?;
let add_action = |svc: &mut Service, action: Arc<Action>| {
svc.add_action(action)
.map_err(|e| FactoryError::ActionError(e.to_string()))
};
// Play
let mut play = Action::new("Play".to_string());
add_arg_in(&mut play, "InstanceID", &AVT_INSTANCE_ID)?;
@@ -304,31 +295,21 @@ impl WebRendererFactory {
Ok(svc)
}
/// Construit le service RenderingControl avec les handlers pipeline
fn build_renderingcontrol(
pipeline: PipelineHandle,
state: SharedState,
) -> Result<Service, FactoryError> {
/// Construit le service RenderingControl
fn build_renderingcontrol(state: SharedState) -> Result<Service, FactoryError> {
let mut svc = Service::new("RenderingControl".to_string());
svc.add_variable(Arc::clone(&RC_INSTANCE_ID))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&A_ARG_TYPE_CHANNEL))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&VOLUME))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&MUTE))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
add_var(&mut svc, &RC_INSTANCE_ID)?;
add_var(&mut svc, &A_ARG_TYPE_CHANNEL)?;
add_var(&mut svc, &VOLUME)?;
add_var(&mut svc, &MUTE)?;
// SetVolume
let mut set_vol = Action::new("SetVolume".to_string());
add_arg_in(&mut set_vol, "InstanceID", &RC_INSTANCE_ID)?;
add_arg_in(&mut set_vol, "Channel", &A_ARG_TYPE_CHANNEL)?;
add_arg_in(&mut set_vol, "DesiredVolume", &VOLUME)?;
set_vol.set_handler(handlers::set_volume_handler(
pipeline.clone(),
state.clone(),
));
set_vol.set_handler(handlers::set_volume_handler(state.clone()));
add_action(&mut svc, Arc::new(set_vol))?;
// GetVolume
@@ -345,7 +326,7 @@ impl WebRendererFactory {
add_arg_in(&mut set_mute, "InstanceID", &RC_INSTANCE_ID)?;
add_arg_in(&mut set_mute, "Channel", &A_ARG_TYPE_CHANNEL)?;
add_arg_in(&mut set_mute, "DesiredMute", &MUTE)?;
set_mute.set_handler(handlers::set_mute_handler(pipeline.clone(), state.clone()));
set_mute.set_handler(handlers::set_mute_handler(state.clone()));
add_action(&mut svc, Arc::new(set_mute))?;
// GetMute
@@ -355,8 +336,7 @@ impl WebRendererFactory {
add_arg_out(&mut get_mute, "CurrentMute", &MUTE)?;
get_mute.set_stateful(false);
get_mute.set_handler(handlers::get_mute_handler(state.clone()));
svc.add_action(Arc::new(get_mute))
.map_err(|e| FactoryError::ActionError(format!("{:?}", e)))?;
add_action(&mut svc, Arc::new(get_mute))?;
Ok(svc)
}