♻️ Reorganize crates architecture (T5) — separate core and browser adapters

- Migrate pmomediarenderer: extract all core logic (adapter, handlers, pipeline etc.) into dedicated modules
- Rename types to MediaRenderer* for clarity (Web → Mediarenderer)
pmowebrenderer becomes a pure browser adapter crate
- Update Cargo.toml dependencies accordingly, add pmoserver feature propagation  
PMOMusic: remove obsolete MEDIA_RENDERER import
- Update all imports across workspace to reflect new crate boundaries
This commit is contained in:
2026-04-05 21:20:51 +02:00
parent fe2d755b4a
commit e375d447d4
26 changed files with 552 additions and 431 deletions

View File

@@ -823,13 +823,334 @@ cargo check -p pmowebrenderer --features pmoserver
---
### T4 — Phase 5 : Restructuration `core/` vs `browser/` ⏸️ Différé
### T4 — Phase 5 : Restructuration `core/` vs `browser/` ✅ Remplacé et réalisé via T5
À faire quand les interfaces sont stabilisées.
Voir la section "Phase 5" du plan ci-dessus pour l'ordre de déplacement.
Condition : `cargo check -p pmowebrenderer` doit passer à chaque étape.
Cette phase est **remplacée et amplifiée** par T5 ci-dessous : au lieu de créer des
sous-répertoires `core/` et `browser/` dans `pmowebrenderer`, on va séparer les deux
niveaux en crates distinctes.
**Écart mineur à surveiller** : `pause_handler` HTTP (`register.rs`) livre `Pause` au browser
via l'adapter mais ne suspend pas le pipeline serveur ni n'appelle `flac_handle.pause()`.
Non bloquant (le browser passe par UPnP pour les vraies pauses), mais à aligner si cet
endpoint est utilisé directement à l'avenir.
---
## Plan T5 — Séparation crates : core dans `pmomediarenderer`, adapter browser dans `pmowebrenderer`
### Contexte
La phase 5 originale (sous-répertoires `core/` vs `browser/`) est insuffisante. La vraie
séparation architecturale est au niveau des **crates** :
- `pmomediarenderer` : déjà le bon endroit pour la logique générique d'un MediaRenderer UPnP
(pipeline audio, handlers UPnP, registry d'instances, factory de devices). Actuellement
réduite à des définitions déclaratives (variables, actions, device statique sans handler).
- `pmowebrenderer` : doit devenir un **adaptateur pur** pour le rendu dans un navigateur.
HTTP polling, streaming OGG-FLAC, enregistrement — tout ce qui est spécifique au browser.
Ajouter Android Auto = créer `pmomandroidrenderer` qui dépend de `pmomediarenderer`
(core), sans toucher ni à `pmomediarenderer` ni à `pmowebrenderer`.
### État actuel des crates
```
pmomediarenderer (déclaratif uniquement, ~64 fichiers)
avtransport/ ← définitions de variables UPnP (conservées)
renderingcontrol/ ← définitions de variables UPnP (conservées)
connectionmanager/ ← définitions de variables UPnP (conservées)
device.rs ← MEDIA_RENDERER statique sans handler → À SUPPRIMER
Cargo.toml ← dépendances : pmoupnp, pmodidl, once_cell
pmowebrenderer (core + adapter mélangés, ~1900 lignes)
adapter.rs ← DeviceAdapter trait + DeviceCommand + BrowserAdapter
handlers.rs ← handlers UPnP AVTransport/RenderingControl → DÉPLACER
pipeline.rs ← pipeline audio PlayerSource + OggFlac → DÉPLACER
renderer.rs ← WebRendererFactory (crée devices UPnP) → DÉPLACER
registry.rs ← RendererRegistry + instances → DÉPLACER
state.rs ← RendererState → DÉPLACER
messages.rs ← PlaybackState → DÉPLACER
error.rs ← WebRendererError → DÉPLACER
register.rs ← HTTP endpoints (register/command/report...) → CONSERVER
stream.rs ← GET /stream → CONSERVER
config.rs ← WebRendererExt (pmoserver) → CONSERVER
```
### Architecture cible
```
pmomediarenderer/src/
lib.rs ← exports publics du core
avtransport/ ← CONSERVÉ (variables statiques réutilisées par renderer.rs)
renderingcontrol/ ← CONSERVÉ
connectionmanager/ ← CONSERVÉ
device.rs ← SUPPRIMÉ (MEDIA_RENDERER sans handler n'est plus utile)
adapter.rs ← NOUVEAU : DeviceAdapter trait + DeviceCommand (ex pmowebrenderer)
handlers.rs ← NOUVEAU : handlers UPnP → pipeline
pipeline.rs ← NOUVEAU : pipeline audio + run_event_listener
renderer.rs ← NOUVEAU : MediaRendererFactory (ex WebRendererFactory)
registry.rs ← NOUVEAU : MediaRendererRegistry + MediaRendererInstance
state.rs ← NOUVEAU : RendererState + SharedState
messages.rs ← NOUVEAU : PlaybackState
error.rs ← NOUVEAU : MediaRendererError (ex WebRendererError)
pmowebrenderer/src/
lib.rs ← exports : BrowserAdapter + WebRendererExt
adapter.rs ← RÉDUIT : BrowserAdapter uniquement
(DeviceAdapter/DeviceCommand importés de pmomediarenderer)
register.rs ← CONSERVÉ (HTTP endpoints, imports depuis pmomediarenderer)
stream.rs ← CONSERVÉ
config.rs ← CONSERVÉ (WebRendererExt, feature pmoserver)
```
### Renommages
| Avant (pmowebrenderer) | Après (pmomediarenderer) |
|---|---|
| `WebRendererFactory` | `MediaRendererFactory` |
| `WebRendererInstance` | `MediaRendererInstance` |
| `WebRendererError` | `MediaRendererError` |
| `RendererRegistry` | `MediaRendererRegistry` |
### Dépendances après refactoring
```toml
# pmomediarenderer/Cargo.toml — NOUVELLES dépendances à ajouter
pmoaudio-ext = { path = "../pmoaudio-ext", features = ["http-stream"] }
pmoaudio = { path = "../pmoaudio" }
pmoflac = { path = "../pmoflac" }
pmoconfig = { path = "../pmoconfig" }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
parking_lot = "0.12"
thiserror = { workspace = true }
tracing = { workspace = true }
pmoutils = { version = "0.1.2", registry = "pmo" }
# Optionnelles
pmoserver = { path = "../pmoserver", optional = true }
pmocontrol = { path = "../pmocontrol", optional = true }
[features]
default = []
pmoserver = ["dep:pmoserver", "dep:pmocontrol"]
# pmowebrenderer/Cargo.toml — APRÈS refactoring
# Supprimer : pmoaudio, pmoaudio-ext, pmoflac, pmometadata, tokio-util, parking_lot,
# thiserror, uuid, pmodidl, pmoutils
# Ajouter : pmomediarenderer = { path = "../pmomediarenderer", features = [] }
# Conserver : axum, axum-extra, tower-http, futures, reqwest, bytes, utoipa, serde,
# serde_json, tokio, pmoserver(opt), pmocontrol(opt), pmoconfig
```
### Plan d'exécution
#### Étape 5.1 — Mettre à jour `pmomediarenderer/Cargo.toml`
Ajouter toutes les nouvelles dépendances listées ci-dessus. Ajouter `[features]` avec `pmoserver`.
`cargo check -p pmomediarenderer` doit toujours compiler (pas encore de nouveau code).
#### Étape 5.2 — Déplacer les modules "socle" (pas de dépendances internes)
Dans l'ordre (du moins couplé au plus couplé) :
1. `messages.rs``pmomediarenderer/src/messages.rs` (dépend de : serde seul)
2. `error.rs``pmomediarenderer/src/error.rs` (renommer `WebRendererError``MediaRendererError`)
3. `state.rs``pmomediarenderer/src/state.rs` (dépend de : messages, adapter)
4. `adapter.rs` (trait + enum) → `pmomediarenderer/src/adapter.rs`
Mettre à jour `pmomediarenderer/src/lib.rs` à chaque fichier ajouté.
`cargo check -p pmomediarenderer` à chaque étape.
#### Étape 5.3 — Déplacer `pipeline.rs`
Dépend de : state, messages, pmoaudio-ext, pmoflac, tokio.
Adapter les imports `crate::` → rester valides dans le nouveau contexte.
Garder `#[cfg(feature = "pmoserver")]` sur la section `ControlPoint`.
`cargo check -p pmomediarenderer`.
#### Étape 5.4 — Déplacer `handlers.rs`
Dépend de : messages, pipeline, state, pmodidl, pmoupnp.
Adapter les imports. Pas de renommage de fonctions à ce stade.
`cargo check -p pmomediarenderer`.
#### Étape 5.5 — Déplacer `renderer.rs` et renommer
- Déplacer vers `pmomediarenderer/src/renderer.rs`
- Renommer `WebRendererFactory``MediaRendererFactory`
- Mettre à jour les imports (variables UPnP maintenant dans `crate::avtransport::*` — déjà le même module)
- `cargo check -p pmomediarenderer`
#### Étape 5.6 — Déplacer `registry.rs` et renommer
- Déplacer vers `pmomediarenderer/src/registry.rs`
- Renommer `RendererRegistry``MediaRendererRegistry`, `WebRendererInstance``MediaRendererInstance`
- Mettre à jour les appels à `WebRendererFactory``MediaRendererFactory`
- `cargo check -p pmomediarenderer`
#### Étape 5.7 — Supprimer `device.rs` de `pmomediarenderer`
Vérifier que PMOMusic n'utilise `MEDIA_RENDERER` que comme point d'entrée obsolète.
Chercher tous les usages de `MEDIA_RENDERER` et `pmomediarenderer::MEDIA_RENDERER` dans le workspace.
Si PMOMusic l'utilise, adapter `PMOMusic/src/main.rs` pour utiliser `MediaRendererRegistry` à la place.
Supprimer `device.rs` et son export dans `lib.rs`.
`cargo check --workspace`.
#### Étape 5.8 — Réduire `pmowebrenderer`
1. Réduire `adapter.rs` à `BrowserAdapter` seul :
```rust
use pmomediarenderer::adapter::{DeviceAdapter, DeviceCommand, DeviceStateReport};
pub struct BrowserAdapter { pub state: pmomediarenderer::state::SharedState }
impl DeviceAdapter for BrowserAdapter { ... }
```
2. Mettre à jour `register.rs` : remplacer `crate::registry::RendererRegistry` →
`pmomediarenderer::registry::MediaRendererRegistry`, idem pour les autres types.
3. Mettre à jour `stream.rs` : imports depuis pmomediarenderer.
4. Mettre à jour `config.rs` : imports depuis pmomediarenderer.
5. Mettre à jour `pmowebrenderer/Cargo.toml` : supprimer les dépendances migrées, ajouter
`pmomediarenderer`.
6. Mettre à jour `lib.rs` pour ne plus exporter que les types browser-spécifiques.
`cargo check -p pmowebrenderer`.
#### Étape 5.9 — Vérification finale workspace
```bash
cargo check --workspace
cargo check --workspace --features pmoserver
# Test fonctionnel : BubbleUPnP → SetAVTransportURI → Play → stream browser OK
```
### Points d'attention
**`pmoserver` feature propagation** : `pmowebrenderer` active `pmoserver` via
`features = ["pmoserver"]` sur la dépendance `pmomediarenderer`. Les deux crates auront leur
propre feature flag `pmoserver`, mais `WebRendererExt` dans `pmowebrenderer` dépend du
feature activé dans `pmomediarenderer`.
**Chemins de types publics** : tout code client qui importe
`pmowebrenderer::{RendererRegistry, WebRendererFactory, ...}` devra mettre à jour ses imports
vers `pmomediarenderer::registry::MediaRendererRegistry` etc. Vérifier `PMOMusic/src/main.rs`
en priorité.
**`pmoutils` / `pmometadata`** : vérifier si utilisés dans les fichiers déplacés. Si oui,
ajouter à `pmomediarenderer/Cargo.toml`.
### Vérification finale
```bash
# 1. Compilation
cargo check -p pmomediarenderer
cargo check -p pmomediarenderer --features pmoserver
cargo check -p pmowebrenderer --features pmoserver
cargo check --workspace --features pmoserver
# 2. Test fonctionnel (inchangé par rapport au plan précédent)
# BubbleUPnP → SetAVTransportURI + Play → stream OGG-FLAC dans le navigateur
# Pause / Stop / Next (transitions de piste < 1s)
# Reconnexion navigateur (reload page)
# GET /api/webrenderer/{id}/nowplaying et /state
```
---
## Rapport d'exécution T5 (2026-04-05)
### Ce qui a été réalisé ✅
La migration est **structurellement complète** :
| Élément | Statut | Notes |
|---------|--------|-------|
| `pmomediarenderer/src/adapter.rs` | ✅ | `DeviceAdapter` trait + `DeviceCommand` enum + `DeviceStateReport` |
| `pmomediarenderer/src/handlers.rs` | ✅ | Handlers UPnP AVTransport / RenderingControl |
| `pmomediarenderer/src/pipeline.rs` | ✅ | Pipeline audio + `run_event_listener` avec `Weak<dyn DeviceAdapter>` |
| `pmomediarenderer/src/renderer.rs` | ✅ | `MediaRendererFactory` (ex `WebRendererFactory`) |
| `pmomediarenderer/src/registry.rs` | ✅ | `MediaRendererRegistry` + `MediaRendererInstance` (renommés) |
| `pmomediarenderer/src/state.rs` | ✅ | `RendererState` + `SharedState` |
| `pmomediarenderer/src/messages.rs` | ✅ | `PlaybackState` |
| `pmomediarenderer/src/error.rs` | ✅ | `MediaRendererError` (ex `WebRendererError`) |
| `pmomediarenderer/Cargo.toml` | ✅ | Toutes les nouvelles dépendances ajoutées, feature `pmoserver` |
| `pmomediarenderer/src/device.rs` | ✅ | Supprimé |
| `pmowebrenderer/src/lib.rs` | ✅ | Réduit à `adapter`, `register`, `stream`, `config` |
| `pmowebrenderer/src/adapter.rs` | ✅ | `BrowserAdapter` uniquement, imports depuis `pmomediarenderer` |
| `pmowebrenderer/src/register.rs` | ✅ | Imports depuis `pmomediarenderer` |
| `pmowebrenderer/src/stream.rs` | ✅ | Imports depuis `pmomediarenderer` |
| `pmowebrenderer/src/config.rs` | ✅ | Imports depuis `pmomediarenderer` |
### Ce qui reste à faire → T6
---
## T6 — Nettoyage post-migration
### T6.1 — Supprimer les fichiers orphelins de `pmowebrenderer/src/`
Les anciens fichiers du core sont toujours présents dans `pmowebrenderer/src/` mais ne sont
**plus déclarés dans `lib.rs`** — ils sont invisibles au compilateur mais polluent le dépôt.
rust-analyzer émet un warning "unlinked-file" sur chacun.
**Fichiers à supprimer** :
```
pmowebrenderer/src/messages.rs
pmowebrenderer/src/error.rs
pmowebrenderer/src/state.rs
pmowebrenderer/src/pipeline.rs
pmowebrenderer/src/handlers.rs
pmowebrenderer/src/renderer.rs
pmowebrenderer/src/registry.rs
```
```bash
# Vérifier qu'aucun n'est importé depuis l'extérieur avant de supprimer
cargo check -p pmowebrenderer --features pmoserver
# Puis supprimer et vérifier
cargo check -p pmowebrenderer --features pmoserver
```
### T6.2 — Nettoyer `pmowebrenderer/Cargo.toml`
Les dépendances suivantes n'ont plus d'utilisateurs dans `pmowebrenderer` après la migration
et peuvent être supprimées (elles sont maintenant des dépendances de `pmomediarenderer`) :
```toml
# À SUPPRIMER de pmowebrenderer/Cargo.toml :
pmoaudio-ext # plus utilisé dans register.rs/stream.rs/adapter.rs/config.rs
pmoaudio # idem
pmoflac # idem
pmometadata # idem
parking_lot # idem
uuid # idem
thiserror # idem
pmodidl # idem
pmoutils # idem
# pmoupnp # à vérifier : plus utilisé directement ?
# async-trait # utilisé dans config.rs → CONSERVER
# tokio-util # utilisé dans stream.rs (ReaderStream) → CONSERVER
```
Procédure : supprimer une dépendance à la fois, `cargo check -p pmowebrenderer` après chaque.
### T6.3 — Corriger l'import inutilisé dans `pmomediarenderer/src/adapter.rs`
```
⚠ unused import: `std::collections::VecDeque` (ligne 2)
```
Supprimer la ligne `use std::collections::VecDeque;`.
### Vérification T6
```bash
cargo check --workspace --features pmoserver
# Zéro warning "unlinked-file", zéro warning "unused import" dans les deux crates
```

27
Cargo.lock generated
View File

@@ -4027,9 +4027,25 @@ dependencies = [
"bevy_reflect",
"htmlescape",
"once_cell",
"parking_lot",
"pmoaudio",
"pmoaudio-ext",
"pmoconfig",
"pmocontrol",
"pmodidl",
"pmoflac",
"pmometadata",
"pmoserver",
"pmoupnp",
"pmoutils",
"quick-xml",
"serde",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tracing",
"uuid",
]
[[package]]
@@ -4329,27 +4345,16 @@ dependencies = [
"bytes",
"futures",
"parking_lot",
"pmoaudio",
"pmoaudio-ext",
"pmoconfig",
"pmocontrol",
"pmodidl",
"pmoflac",
"pmomediarenderer",
"pmometadata",
"pmoserver",
"pmoupnp",
"pmoutils",
"reqwest",
"serde",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tower-http",
"tracing",
"utoipa",
"uuid",
]
[[package]]

View File

@@ -1,6 +1,5 @@
use pmoapp::{WebAppExt, Webapp};
use pmocontrol::ControlPointExt;
use pmomediarenderer::MEDIA_RENDERER;
use pmomediaserver::{
MEDIA_SERVER, MediaServerDeviceExt, ParadiseStreamingExt, sources::SourcesExt,
};
@@ -71,17 +70,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Enregistrer les devices UPnP (HTTP + SSDP automatique)
info!("📡 Registering UPnP devices...");
let renderer_instance = server
.write()
.await
.register_device(MEDIA_RENDERER.clone(), true)
.await
.expect("Failed to register MediaRenderer");
let base_url = renderer_instance.base_url();
let desc_route = renderer_instance.description_route();
info!("✅ MediaRenderer ready at {}{}", base_url, desc_route);
let server_instance = server
.write()
.await

View File

@@ -1,7 +1,7 @@
[package]
name = "pmomediarenderer"
version = "0.1.0"
edition = "2024"
edition = "2021"
[dependencies]
pmoupnp = { path = "../pmoupnp" }
@@ -11,3 +11,28 @@ once_cell = "1.20"
bevy_reflect = "0.17.1"
htmlescape = "0.3"
quick-xml = { workspace = true }
pmoaudio-ext = { path = "../pmoaudio-ext", features = ["http-stream"] }
pmoaudio = { path = "../pmoaudio" }
pmoflac = { path = "../pmoflac" }
pmoconfig = { path = "../pmoconfig" }
pmometadata = { path = "../pmometadata" }
pmoutils = { version = "0.1.2", registry = "pmo" }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
parking_lot = "0.12"
thiserror = { workspace = true }
tracing = { workspace = true }
pmoserver = { path = "../pmoserver", optional = true }
pmocontrol = { path = "../pmocontrol", optional = true }
[features]
default = []
pmoserver = ["dep:pmoserver", "dep:pmocontrol"]

View File

@@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DeviceCommand {
Stream { url: String },
Play,
Pause,
Seek { position_sec: f64 },
Flush,
Stop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DevicePlaybackState {
Playing,
Paused,
Stopped,
Buffering,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceStateReport {
pub position_sec: Option<f64>,
pub duration_sec: Option<f64>,
pub playback_state: Option<DevicePlaybackState>,
}
pub trait DeviceAdapter: Send + Sync + 'static {
fn deliver(&self, command: DeviceCommand);
fn poll_state(&self) -> Option<DeviceStateReport>;
}

View File

@@ -1,6 +1,6 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI,
AVTRANSPORTURIMETADATA, CURRENTTRACK, NUMBEROFTRACKS,
AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
A_ARG_TYPE_INSTANCE_ID, CURRENTTRACK, NUMBEROFTRACKS,
};
use pmoupnp::define_action;

View File

@@ -1,5 +1,5 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, ABSOLUTETIMEPOSITION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
ABSOLUTETIMEPOSITION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID,
CURRENTTRACK, CURRENTTRACKDURATION, RELATIVETIMEPOSITION,
};
use pmoupnp::define_action;

View File

@@ -1,5 +1,5 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA,
AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, A_ARG_TYPE_INSTANCE_ID,
};
use pmoupnp::define_action;

View File

@@ -1,5 +1,5 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
AVTRANSPORTURI, AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID,
};
use pmoupnp::define_action;

View File

@@ -100,8 +100,8 @@ use actions::{
SETNEXTAVTRANSPORTURI, STOP,
};
use variables::{
A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE, ABSOLUTETIMEPOSITION,
AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
ABSOLUTETIMEPOSITION, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI,
AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE,
CURRENTMEDIADURATION, CURRENTPLAYMODE, CURRENTTRACK, CURRENTTRACKDURATION,
CURRENTTRACKMETADATA, CURRENTTRACKURI, NUMBEROFTRACKS, PLAYBACKSTORAGEMEDIUM,
POSSIBLEPLAYBACKSTORAGEMEDIA, RELATIVETIMEPOSITION, SEEKMODE, TRANSPORTPLAYSPEED,

View File

@@ -1,66 +0,0 @@
//! Définition du device MediaRenderer.
use once_cell::sync::Lazy;
use std::sync::Arc;
use crate::{
avtransport::AVTTRANSPORT, connectionmanager::CONNECTIONMANAGER,
renderingcontrol::RENDERINGCONTROL,
};
use pmoupnp::devices::Device;
/// Device MediaRenderer UPnP.
///
/// MediaRenderer audio-only conforme UPnP AV Architecture 1.0.
///
/// # Services inclus
///
/// - **AVTransport:1** : Contrôle de la lecture
/// - **RenderingControl:1** : Contrôle du volume et du mute
/// - **ConnectionManager:1** : Gestion des connexions
///
/// # Spécifications
///
/// - Device Type : `urn:schemas-upnp-org:device:MediaRenderer:1`
/// - Version : 1
/// - Manufacturer : PMOMusic
/// - Model : PMOMusic Audio Renderer
///
/// # Exemple
///
/// ```ignore
/// use pmomediarenderer::MEDIA_RENDERER;
/// use pmoupnp::UpnpModel;
///
/// // Créer une instance du renderer
/// let renderer_instance = MEDIA_RENDERER.create_instance();
///
/// // Accéder aux services
/// if let Some(avtransport) = renderer_instance.get_service("AVTransport") {
/// // Contrôler la lecture...
/// }
/// ```
pub static MEDIA_RENDERER: Lazy<Arc<Device>> = Lazy::new(|| {
let mut device = Device::new_from_config(
"PMO_MediaRenderer".to_string(),
"MediaRenderer".to_string(),
"Audio Renderer".to_string(),
);
device.set_model_description("UPnP AV MediaRenderer for audio streaming".to_string());
// Ajouter les trois services obligatoires
device
.add_service(Arc::clone(&AVTTRANSPORT))
.expect("Failed to add AVTransport service");
device
.add_service(Arc::clone(&RENDERINGCONTROL))
.expect("Failed to add RenderingControl service");
device
.add_service(Arc::clone(&CONNECTIONMANAGER))
.expect("Failed to add ConnectionManager service");
Arc::new(device)
});

View File

@@ -1,9 +1,9 @@
//! Erreurs liées au WebRenderer
//! Erreurs liées au MediaRenderer
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WebRendererError {
pub enum MediaRendererError {
#[error("Invalid argument: {0}")]
InvalidArgument(String),

View File

@@ -1,4 +1,4 @@
//! Action handlers SOAP → Pipeline pour le WebRenderer serveur
//! Action handlers SOAP → Pipeline pour le MediaRenderer
//!
//! 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.
@@ -21,10 +21,10 @@ pub fn play_handler(
) -> ActionHandler {
action_handler!(
captures(pipeline, state, instance_id) | data | {
tracing::info!("[WebRenderer] UPnP Play action invoked");
tracing::info!("[MediaRenderer] UPnP Play action invoked");
let has_uri = state.read().current_uri.is_some();
if !has_uri {
tracing::warn!("[WebRenderer] UPnP Play ignored: no URI loaded");
tracing::warn!("[MediaRenderer] UPnP Play ignored: no URI loaded");
return Ok(data);
}
{
@@ -33,7 +33,6 @@ pub fn play_handler(
s.push_command(crate::adapter::DeviceCommand::Stream {
url: format!("/api/webrenderer/{}/stream", instance_id),
});
tracing::info!("UPnP Play: stored stream command for frontend polling");
}
pipeline.flac_handle.resume();
pipeline.send(PipelineControl::Play).await;
@@ -106,7 +105,7 @@ pub fn seek_handler(pipeline: PipelineHandle) -> ActionHandler {
pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
action_handler!(captures(pipeline, state) |mut data| {
tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked");
tracing::info!("[MediaRenderer] 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()))
@@ -159,7 +158,6 @@ pub fn get_position_info_handler(state: SharedState) -> ActionHandler {
pub fn get_transport_info_handler(state: SharedState) -> ActionHandler {
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",

View File

@@ -11,25 +11,23 @@
//! - **AVTransport** : Contrôle de la lecture (play, pause, stop, seek, etc.)
//! - **RenderingControl** : Contrôle du volume et du mute
//! - **ConnectionManager** : Gestion des connexions et des protocoles supportés
//!
//! # Device UPnP
//!
//! - Type : `urn:schemas-upnp-org:device:MediaRenderer:1`
//! - Services : AVTransport:1, RenderingControl:1, ConnectionManager:1
//!
//! # Utilisation
//!
//! ```ignore
//! use pmomediarenderer::MEDIA_RENDERER;
//!
//! // Le device est déjà configuré avec tous ses services
//! let renderer = MEDIA_RENDERER.clone();
//! let instance = renderer.create_instance();
//! ```
pub mod adapter;
pub mod avtransport;
pub mod connectionmanager;
pub mod device;
pub mod error;
pub mod handlers;
pub mod messages;
pub mod pipeline;
pub mod registry;
pub mod renderingcontrol;
pub mod renderer;
pub mod state;
pub use device::MEDIA_RENDERER;
pub use error::MediaRendererError;
pub use handlers::*;
pub use messages::{PlaybackState, PlayerStateReport};
pub use pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds, InstancePipeline};
pub use registry::{MediaRendererInstance, MediaRendererRegistry};
pub use state::{RendererState, SharedState};
pub use adapter::{DeviceAdapter, DeviceCommand, DevicePlaybackState, DeviceStateReport};

View File

@@ -0,0 +1,21 @@
//! Types de messages pour le MediaRenderer
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PlaybackState {
Stopped,
Playing,
Paused,
Transitioning,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct PlayerStateReport {
pub position_sec: Option<f64>,
pub duration_sec: Option<f64>,
pub state: Option<String>,
pub ready_state: Option<String>,
}

View File

@@ -1,9 +1,9 @@
//! Pipeline audio serveur par instance WebRenderer
//! Pipeline audio serveur par instance MediaRenderer
//!
//! Chaque instance WebRenderer possède un pipeline indépendant :
//! - Une `PlayerSource` qui gère le cycle de vie AVTransport (Play/Pause/Stop/Seek/LoadUri)
//! - Un `StreamingOggFlacSink` qui encode et diffuse le flux OGG-FLAC aux clients HTTP
//! - Des nœuds de normalisation (resampling → 96 kHz, conversion → I24)
//! Chaque instance MediaRenderer possède un pipeline独立的音频处理:
//! - 一个 `PlayerSource` 管理 AVTransport 生命周期(Play/Pause/Stop/Seek/LoadUri
//! - 一个 `StreamingOggFlacSink` 编码并向 HTTP 客户端传输 OGG-FLAC 流
//! - 规范化节点(重采样 → 96 kHz转换 → I24
use std::sync::Arc;
use pmoaudio::{ResamplingNode, ToI24Node};
@@ -17,15 +17,10 @@ use crate::state::SharedState;
// ─── Ré-export des commandes pour les handlers ────────────────────────────────
/// Commandes de transport — alias vers PlayerCommand pour compatibilité handlers
pub use pmoaudio_ext::PlayerCommand as PipelineControl;
// ─── Handle vers le pipeline ─────────────────────────────────────────────────
/// Handle partageable vers le pipeline audio d'une instance.
///
/// Expose le `PlayerHandle` pour les commandes AVTransport et le `CancellationToken`
/// pour l'arrêt complet du pipeline.
#[derive(Clone)]
pub struct PipelineHandle {
pub player: PlayerHandle,
@@ -37,7 +32,6 @@ pub struct PipelineHandle {
}
impl PipelineHandle {
/// Envoie une commande de transport. Gère SetVolume/SetMute localement.
pub async fn send(&self, cmd: PipelineControl) {
match cmd {
PlayerCommand::LoadUri(uri) => self.player.load_uri(uri).await,
@@ -52,19 +46,12 @@ impl PipelineHandle {
// ─── Pipeline instancié ──────────────────────────────────────────────────────
/// Pipeline audio complet pour une instance WebRenderer.
///
/// Créé au `POST /register`. Le flux OGG-FLAC est accessible via `flac_handle`
/// (multi-client broadcast, chaque `subscribe()` crée un flux indépendant).
pub struct InstancePipeline {
/// Handle vers le sink OGG-FLAC — clonable, subscribe() crée un flux indépendant par client.
pub flac_handle: OggFlacStreamHandle,
pub pipeline_handle: PipelineHandle,
}
impl InstancePipeline {
/// Crée et démarre le pipeline en background.
/// Retourne immédiatement avec les handles nécessaires.
pub fn start(
state: SharedState,
#[cfg(feature = "pmoserver")]
@@ -76,22 +63,17 @@ impl InstancePipeline {
use pmoaudio::pipeline::AudioPipelineNode;
// Sink broadcast OGG-FLAC (multi-client, pacé à 0.5s max d'avance)
let (sink, flac_handle) = StreamingOggFlacSink::new(EncoderOptions::default(), 24);
// Nœud de conversion de profondeur : tout type entier → I24
let mut to_i24 = ToI24Node::new();
to_i24.register(sink.boxed());
// Nœud de rééchantillonnage : n'importe quel sample rate → 96 kHz
let mut resampler = ResamplingNode::new(96_000);
resampler.register(to_i24.boxed());
// Source avec contrôle AVTransport complet
let (mut player_source, player_handle) = PlayerSource::new();
player_source.register(resampler.boxed());
// Lancer le pipeline en background
let sink_stop = stop_token.clone();
tokio::spawn(async move {
if let Err(e) = player_source.boxed().run(sink_stop).await {
@@ -100,7 +82,6 @@ impl InstancePipeline {
debug!("Pipeline task terminated");
});
// Écouter les événements PlayerSource pour mettre à jour le state UPnP
let event_rx = player_handle.subscribe_events();
let state_clone = state.clone();
let udn_clone = udn.clone();
@@ -156,7 +137,6 @@ async fn run_event_listener(
s.current_uri = Some(uri);
s.duration = duration_sec.map(seconds_to_upnp_time);
s.position = None;
// Effacer next_uri/next_metadata : la nouvelle piste est maintenant courante
s.next_uri = None;
s.next_metadata = None;
}
@@ -175,7 +155,6 @@ async fn run_event_listener(
}
PlayerEvent::TrackEnded => {
state.write().playback_state = PlaybackState::Transitioning;
// Vider le buffer du device avant la nouvelle piste.
if let Some(adapter) = adapter.upgrade() {
adapter.deliver(DeviceCommand::Flush);
}

View File

@@ -1,7 +1,4 @@
//! Registre des instances WebRenderer actives.
//!
//! Remplace `SessionManager` et `websocket.rs`. La session est maintenant liée
//! au flux FLAC HTTP, pas à une connexion WebSocket.
//! Registre des instances MediaRenderer actives.
use parking_lot::RwLock;
use std::collections::HashMap;
@@ -10,10 +7,11 @@ use std::time::SystemTime;
use pmoupnp::devices::DeviceInstance;
use crate::error::WebRendererError;
use crate::pipeline::{InstancePipeline, PipelineControl, PipelineHandle};
use crate::renderer::WebRendererFactory;
use crate::error::MediaRendererError;
use crate::pipeline::{InstancePipeline, PipelineHandle};
use crate::renderer::MediaRendererFactory;
use crate::state::{RendererState, SharedState};
use super::adapter::DeviceAdapter;
#[cfg(feature = "pmoserver")]
@@ -23,33 +21,26 @@ use pmocontrol::model::{RendererCapabilities, RendererProtocol};
#[cfg(feature = "pmoserver")]
use pmoupnp::UpnpTypedInstance;
/// Une instance WebRenderer côté serveur
pub struct WebRendererInstance {
pub struct MediaRendererInstance {
pub instance_id: String,
pub udn: String,
pub device_instance: Arc<DeviceInstance>,
pub state: SharedState,
/// Handle vers le sink OGG-FLAC — clonable, subscribe() crée un flux indépendant par client.
pub flac_handle: pmoaudio_ext::sinks::OggFlacStreamHandle,
pub pipeline: PipelineHandle,
pub created_at: SystemTime,
/// Adapter device-spécifique pour la livraison des commandes.
pub adapter: Arc<dyn crate::adapter::DeviceAdapter>,
pub adapter: Arc<dyn DeviceAdapter>,
}
/// Registre global des instances WebRenderer
pub struct RendererRegistry {
/// Map instance_id → instance
instances: RwLock<HashMap<String, Arc<WebRendererInstance>>>,
/// Map udn → instance (pour retrouver depuis les handlers UPnP)
by_udn: RwLock<HashMap<String, Arc<WebRendererInstance>>>,
/// Tokens d'annulation des unregister différés (instance_id → token)
pub struct MediaRendererRegistry {
instances: RwLock<HashMap<String, Arc<MediaRendererInstance>>>,
by_udn: RwLock<HashMap<String, Arc<MediaRendererInstance>>>,
pending_unregister: RwLock<HashMap<String, tokio_util::sync::CancellationToken>>,
#[cfg(feature = "pmoserver")]
control_point: Arc<ControlPoint>,
}
impl RendererRegistry {
impl MediaRendererRegistry {
#[cfg(feature = "pmoserver")]
pub fn new(control_point: Arc<ControlPoint>) -> Self {
Self {
@@ -69,25 +60,21 @@ impl RendererRegistry {
}
}
/// Enregistre ou reconnecte une instance.
/// 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, bool), WebRendererError> {
// Annuler tout unregister différé pour cet instance_id
adapter: Arc<dyn DeviceAdapter>,
) -> Result<(String, String, bool), MediaRendererError> {
if let Some(cancel) = self.pending_unregister.write().remove(instance_id) {
tracing::info!(instance_id = %instance_id, "WebRenderer: cancelled pending unregister (page reload)");
tracing::info!(instance_id = %instance_id, "MediaRenderer: cancelled pending unregister (page reload)");
cancel.cancel();
}
// Reconnexion : l'instance existe déjà (ou vient d'être conservée)
{
let instances = self.instances.read();
if let Some(existing) = instances.get(instance_id) {
tracing::info!(instance_id = %instance_id, "WebRenderer: reconnecting existing instance");
tracing::info!(instance_id = %instance_id, "MediaRenderer: reconnecting existing instance");
#[cfg(feature = "pmoserver")]
self.register_with_control_point(&existing.device_instance)?;
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
@@ -102,8 +89,7 @@ impl RendererRegistry {
}
}
// Première connexion : créer device UPnP + pipeline
let instance = self.create_instance(instance_id, user_agent).await?;
let instance = self.create_instance_with_adapter(instance_id, user_agent, adapter).await?;
let instance = Arc::new(instance);
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
let udn = instance.udn.clone();
@@ -120,14 +106,12 @@ impl RendererRegistry {
tracing::info!(
instance_id = %instance_id,
udn = %udn,
"WebRenderer: new instance registered"
"MediaRenderer: new instance registered"
);
Ok((stream_url, udn, false))
}
/// Retourne un OggFlacClientStream indépendant pour l'endpoint /stream.
/// Chaque appel crée un nouveau subscriber broadcast — safe pour connexions simultanées.
pub fn get_stream(
&self,
instance_id: &str,
@@ -145,7 +129,6 @@ impl RendererRegistry {
}
}
/// Retourne le PipelineHandle par UDN (pour les handlers UPnP)
pub fn get_pipeline_by_udn(&self, udn: &str) -> Option<PipelineHandle> {
self.by_udn
.read()
@@ -153,12 +136,10 @@ impl RendererRegistry {
.map(|i| i.pipeline.clone())
}
/// Retourne l'instance par instance_id
pub fn get_instance(&self, instance_id: &str) -> Option<Arc<WebRendererInstance>> {
pub fn get_instance(&self, instance_id: &str) -> Option<Arc<MediaRendererInstance>> {
self.instances.read().get(instance_id).cloned()
}
/// Retourne le SharedState par instance_id
pub fn get_state(&self, instance_id: &str) -> Option<SharedState> {
self.instances
.read()
@@ -166,12 +147,10 @@ impl RendererRegistry {
.map(|i| i.state.clone())
}
/// Retourne le PipelineHandle par instance_id
pub fn get_pipeline(&self, instance_id: &str) -> Option<PipelineHandle> {
self.instances.read().get(instance_id).map(|i| i.pipeline.clone())
}
/// Retourne le SharedState et udn par instance_id
pub fn get_state_and_udn(&self, instance_id: &str) -> Option<(SharedState, String)> {
self.instances
.read()
@@ -179,7 +158,6 @@ impl RendererRegistry {
.map(|i| (i.state.clone(), i.udn.clone()))
}
/// Retourne le SharedState par UDN
pub fn get_state_by_udn(&self, udn: &str) -> Option<SharedState> {
self.by_udn
.read()
@@ -187,7 +165,6 @@ impl RendererRegistry {
.map(|i| i.state.clone())
}
/// Retourne le DeviceInstance par UDN
pub fn get_device_by_udn(&self, udn: &str) -> Option<Arc<DeviceInstance>> {
self.by_udn
.read()
@@ -195,14 +172,10 @@ impl RendererRegistry {
.map(|i| i.device_instance.clone())
}
/// Met à jour la durée depuis le navigateur.
/// La position est gérée par PlayerSource via PlayerEvent::Position.
/// On n'utilise duration_sec que si la source ne la connaît pas (flux radio sans durée).
pub fn update_duration(&self, instance_id: &str, duration_sec: Option<f64>) {
let instances = self.instances.read();
if let Some(instance) = instances.get(instance_id) {
let mut s = instance.state.write();
// N'écraser la durée que si elle n'est pas déjà connue (la source est prioritaire)
if s.duration.is_none() {
if let Some(dur) = duration_sec {
if dur > 0.0 {
@@ -216,19 +189,18 @@ impl RendererRegistry {
pub fn schedule_unregister(self: &Arc<Self>, instance_id: &str) {
use tokio_util::sync::CancellationToken;
// Ne pas détruire immédiatement : attendre 5s au cas où la page se recharge
let cancel = CancellationToken::new();
self.pending_unregister.write().insert(instance_id.to_string(), cancel.clone());
let instance_id_owned = instance_id.to_string();
let registry = Arc::clone(self);
tracing::info!(instance_id = %instance_id, "WebRenderer: unregister scheduled (5s grace period)");
tracing::info!(instance_id = %instance_id, "MediaRenderer: unregister scheduled (5s grace period)");
tokio::spawn(async move {
tokio::select! {
_ = cancel.cancelled() => {
tracing::info!(instance_id = %instance_id_owned, "WebRenderer: deferred unregister cancelled (page reload)");
tracing::info!(instance_id = %instance_id_owned, "MediaRenderer: deferred unregister cancelled (page reload)");
}
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
registry.pending_unregister.write().remove(&instance_id_owned);
@@ -243,7 +215,7 @@ impl RendererRegistry {
tracing::info!(
instance_id = %instance_id_owned,
udn = %instance.udn,
"WebRenderer: instance unregistered"
"MediaRenderer: instance unregistered"
);
}
}
@@ -251,11 +223,10 @@ 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,
report: crate::messages::PlayerStateReport,
) {
let instances = self.instances.read();
if let Some(instance) = instances.get(instance_id) {
@@ -278,7 +249,6 @@ impl RendererRegistry {
}
}
/// Récupère et consomme la commande en attente pour le player
pub async fn get_pending_command(
&self,
instance_id: &str,
@@ -288,40 +258,33 @@ impl RendererRegistry {
serde_json::to_value(cmd).ok()
}
// ── Création d'instance ────────────────────────────────────────────────────
async fn create_instance(
/// Créer une nouvelle instance avec un adapter fourni (permet à l'appelant de créer BrowserAdapter)
pub async fn create_instance_with_adapter(
&self,
instance_id: &str,
user_agent: &str,
) -> Result<WebRendererInstance, WebRendererError> {
// UDN stable dérivé de l'instance_id
adapter: Arc<dyn DeviceAdapter>,
) -> Result<MediaRendererInstance, MediaRendererError> {
let candidate_udn = instance_id.to_ascii_lowercase();
let full_udn = format!("uuid:{}", candidate_udn);
// Persister l'UDN dans la config (pour que device_instance.rs le retrouve)
if let Err(e) = pmoconfig::get_config().set_device_udn(
"MediaRenderer",
instance_id,
candidate_udn.clone(),
) {
tracing::warn!("WebRenderer: failed to persist UDN: {:?}", e);
tracing::warn!("MediaRenderer: failed to persist UDN: {:?}", e);
}
let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default()));
// Créer l'adapter avant le pipeline (sera passé à run_event_listener)
let adapter: Arc<dyn crate::adapter::DeviceAdapter> =
Arc::new(crate::adapter::BrowserAdapter::new(state.clone()));
#[cfg(feature = "pmoserver")]
let (device_instance, pipeline) = {
use pmoupnp::UpnpServerExt;
let server_arc = pmoserver::get_server()
.ok_or(WebRendererError::ServerNotAvailable)?;
.ok_or(MediaRendererError::ServerNotAvailable)?;
// Créer le pipeline avec l'adapter pour le event listener
let ip = InstancePipeline::start(
state.clone(),
self.control_point.clone(),
@@ -330,31 +293,30 @@ impl RendererRegistry {
);
let pipeline = ip.pipeline_handle.clone();
// Vérifier si un device avec ce même UDN existe déjà
let existing_di = {
let server = server_arc.read().await;
server.get_device(&candidate_udn)
};
let di = if let Some(di) = existing_di {
tracing::info!(udn = %candidate_udn, "WebRenderer: reusing device from registry");
tracing::info!(udn = %candidate_udn, "MediaRenderer: reusing device from registry");
di
} else {
// Créer le device UPnP
tracing::info!(udn = %candidate_udn, "WebRenderer: creating new device");
let device = WebRendererFactory::create_device_with_pipeline(
tracing::info!(udn = %candidate_udn, "MediaRenderer: creating new device");
let device = MediaRendererFactory::create_device_with_pipeline(
instance_id,
"MediaRenderer",
user_agent,
pipeline.clone(),
state.clone(),
)
.map_err(|e| WebRendererError::DeviceCreationError(e.to_string()))?;
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
let mut server = server_arc.write().await;
server
.register_device(Arc::new(device), false)
.await
.map_err(|e| WebRendererError::RegistrationError(e.to_string()))?
.map_err(|e| MediaRendererError::RegistrationError(e.to_string()))?
};
self.register_with_control_point(&di)?;
@@ -372,18 +334,19 @@ impl RendererRegistry {
);
let pipeline = ip.pipeline_handle.clone();
let device = WebRendererFactory::create_device_with_pipeline(
let device = MediaRendererFactory::create_device_with_pipeline(
instance_id,
"MediaRenderer",
user_agent,
pipeline.clone(),
state.clone(),
)
.map_err(|e| WebRendererError::DeviceCreationError(e.to_string()))?;
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
(Arc::new(device).create_instance(), ip)
};
Ok(WebRendererInstance {
Ok(MediaRendererInstance {
instance_id: instance_id.to_string(),
udn: full_udn,
device_instance,
@@ -395,12 +358,11 @@ impl RendererRegistry {
})
}
/// Enregistre le device dans le ControlPoint
#[cfg(feature = "pmoserver")]
fn register_with_control_point(
&self,
di: &Arc<DeviceInstance>,
) -> Result<(), WebRendererError> {
) -> Result<(), MediaRendererError> {
let base_url = di.base_url().to_string();
let udn = di.udn().to_ascii_lowercase();
let udn_with_prefix = format!("uuid:{}", udn);
@@ -449,7 +411,7 @@ impl RendererRegistry {
registry.push_renderer(&renderer_info, 86400);
}
tracing::info!(udn = %udn, "WebRenderer: registered with ControlPoint");
tracing::info!(udn = %udn, "MediaRenderer: registered with ControlPoint");
Ok(())
}
}

View File

@@ -1,7 +1,4 @@
//! Factory pour créer des instances WebRenderer privées
//!
//! Construit des Device/Service models UPnP dynamiques avec des action handlers
//! qui relaient les commandes SOAP vers le navigateur via WebSocket.
//! Factory pour créer des instances MediaRenderer privées
use pmoupnp::actions::{Action, Argument};
use pmoupnp::devices::Device;
@@ -54,8 +51,7 @@ fn add_action(svc: &mut Service, action: Arc<Action>) -> Result<(), FactoryError
}
// ─── Réimport des variables statiques de pmomediarenderer ───────────────────
// Variables AVTransport
use pmomediarenderer::avtransport::variables::{
use crate::avtransport::variables::{
ABSOLUTETIMEPOSITION, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI,
AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID as AVT_INSTANCE_ID, A_ARG_TYPE_PLAY_SPEED,
A_ARG_TYPE_SEEKMODE, CURRENTMEDIADURATION, CURRENTPLAYMODE, CURRENTTRACK, CURRENTTRACKDURATION,
@@ -64,13 +60,11 @@ use pmomediarenderer::avtransport::variables::{
TRANSPORTSTATE, TRANSPORTSTATUS,
};
// Variables RenderingControl
use pmomediarenderer::renderingcontrol::variables::{
use crate::renderingcontrol::variables::{
A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID as RC_INSTANCE_ID, MUTE, VOLUME,
};
// Variables ConnectionManager
use pmomediarenderer::connectionmanager::variables::{
use crate::connectionmanager::variables::{
A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS,
A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, CURRENTCONNECTIONIDS,
SINKPROTOCOLINFO, SOURCEPROTOCOLINFO,
@@ -86,34 +80,14 @@ pub enum FactoryError {
VariableError(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/") {
"Edge"
} else if ua.contains("OPR/") || ua.contains("Opera") {
"Opera"
} else if ua.contains("Chrome/") {
"Chrome"
} else if ua.contains("Firefox/") {
"Firefox"
} else if ua.contains("Safari/") {
"Safari"
} else {
"Browser"
}
}
/// Factory pour créer des Device UPnP MediaRenderer avec un pipeline audio serveur
pub struct MediaRendererFactory;
/// Factory pour créer des Device UPnP WebRenderer avec un pipeline audio serveur
pub struct WebRendererFactory;
impl WebRendererFactory {
/// Crée un Device model UPnP complet pour un WebRenderer.
///
/// `device_name` sert de clé pour retrouver l'UDN persistant dans la config.
/// `browser_ua` est le User-Agent complet (pour déterminer le nom affiché).
impl MediaRendererFactory {
pub fn create_device_with_pipeline(
device_name: &str,
browser_ua: &str,
device_type: &str,
device_ua: &str,
pipeline: PipelineHandle,
state: SharedState,
) -> Result<Device, FactoryError> {
@@ -121,13 +95,12 @@ impl WebRendererFactory {
let renderingcontrol = Self::build_renderingcontrol(state.clone())?;
let connectionmanager = Self::build_connectionmanager()?;
let short_name = extract_browser_name(browser_ua);
let mut device = Device::new(
device_name.to_string(),
"MediaRenderer".to_string(),
format!("Web Audio {}", short_name),
device_type.to_string(),
device_ua.to_string(),
);
device.set_model_name("WebRenderer".to_string());
device.set_model_name("MediaRenderer".to_string());
device
.add_service(Arc::new(avtransport))
.map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?;
@@ -141,7 +114,6 @@ impl WebRendererFactory {
Ok(device)
}
/// Construit le service AVTransport avec les handlers pipeline
fn build_avtransport(
pipeline: PipelineHandle,
state: SharedState,
@@ -149,7 +121,6 @@ impl WebRendererFactory {
) -> Result<Service, FactoryError> {
let mut svc = Service::new("AVTransport".to_string());
// Ajouter toutes les variables d'état
add_var(&mut svc, &AVT_INSTANCE_ID)?;
add_var(&mut svc, &A_ARG_TYPE_PLAY_SPEED)?;
add_var(&mut svc, &A_ARG_TYPE_SEEKMODE)?;
@@ -173,38 +144,36 @@ impl WebRendererFactory {
add_var(&mut svc, &TRANSPORTSTATE)?;
add_var(&mut svc, &TRANSPORTSTATUS)?;
// Play
let mut play = Action::new("Play".to_string());
add_arg_in(&mut play, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_in(&mut play, "Speed", &TRANSPORTPLAYSPEED)?;
play.set_handler(handlers::play_handler(pipeline.clone(), state.clone(), instance_id.to_string()));
play.set_handler(handlers::play_handler(
pipeline.clone(),
state.clone(),
instance_id.to_string(),
));
add_action(&mut svc, Arc::new(play))?;
// Stop
let mut stop = Action::new("Stop".to_string());
add_arg_in(&mut stop, "InstanceID", &AVT_INSTANCE_ID)?;
stop.set_handler(handlers::stop_handler(pipeline.clone(), state.clone()));
add_action(&mut svc, Arc::new(stop))?;
// Pause
let mut pause = Action::new("Pause".to_string());
add_arg_in(&mut pause, "InstanceID", &AVT_INSTANCE_ID)?;
pause.set_handler(handlers::pause_handler(pipeline.clone(), state.clone()));
add_action(&mut svc, Arc::new(pause))?;
// Next
let mut next = Action::new("Next".to_string());
add_arg_in(&mut next, "InstanceID", &AVT_INSTANCE_ID)?;
next.set_handler(handlers::next_handler(pipeline.clone()));
add_action(&mut svc, Arc::new(next))?;
// Previous
let mut previous = Action::new("Previous".to_string());
add_arg_in(&mut previous, "InstanceID", &AVT_INSTANCE_ID)?;
previous.set_handler(handlers::previous_handler(pipeline.clone()));
add_action(&mut svc, Arc::new(previous))?;
// Seek
let mut seek = Action::new("Seek".to_string());
add_arg_in(&mut seek, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_in(&mut seek, "Unit", &A_ARG_TYPE_SEEKMODE)?;
@@ -212,7 +181,6 @@ impl WebRendererFactory {
seek.set_handler(handlers::seek_handler(pipeline.clone()));
add_action(&mut svc, Arc::new(seek))?;
// SetAVTransportURI
let mut set_uri = Action::new("SetAVTransportURI".to_string());
add_arg_in(&mut set_uri, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_in(&mut set_uri, "CurrentURI", &AVTRANSPORTURI)?;
@@ -220,7 +188,6 @@ impl WebRendererFactory {
set_uri.set_handler(handlers::set_uri_handler(pipeline.clone(), state.clone()));
add_action(&mut svc, Arc::new(set_uri))?;
// SetNextAVTransportURI
let mut set_next_uri = Action::new("SetNextAVTransportURI".to_string());
add_arg_in(&mut set_next_uri, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_in(&mut set_next_uri, "NextURI", &AVTRANSPORTNEXTURI)?;
@@ -235,7 +202,6 @@ impl WebRendererFactory {
));
add_action(&mut svc, Arc::new(set_next_uri))?;
// GetPositionInfo
let mut get_pos = Action::new("GetPositionInfo".to_string());
add_arg_in(&mut get_pos, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_out(&mut get_pos, "Track", &CURRENTTRACK)?;
@@ -248,7 +214,6 @@ impl WebRendererFactory {
get_pos.set_handler(handlers::get_position_info_handler(state.clone()));
add_action(&mut svc, Arc::new(get_pos))?;
// GetTransportInfo
let mut get_info = Action::new("GetTransportInfo".to_string());
add_arg_in(&mut get_info, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_out(&mut get_info, "CurrentTransportState", &TRANSPORTSTATE)?;
@@ -258,7 +223,6 @@ impl WebRendererFactory {
get_info.set_handler(handlers::get_transport_info_handler(state.clone()));
add_action(&mut svc, Arc::new(get_info))?;
// GetMediaInfo
let mut get_media = Action::new("GetMediaInfo".to_string());
add_arg_in(&mut get_media, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_out(&mut get_media, "NrTracks", &NUMBEROFTRACKS)?;
@@ -278,17 +242,14 @@ impl WebRendererFactory {
get_media.set_handler(handlers::get_media_info_handler(state.clone()));
add_action(&mut svc, Arc::new(get_media))?;
// GetTransportSettings (passthrough)
let mut get_settings = Action::new("GetTransportSettings".to_string());
add_arg_in(&mut get_settings, "InstanceID", &AVT_INSTANCE_ID)?;
add_action(&mut svc, Arc::new(get_settings))?;
// GetDeviceCapabilities (passthrough)
let mut get_caps = Action::new("GetDeviceCapabilities".to_string());
add_arg_in(&mut get_caps, "InstanceID", &AVT_INSTANCE_ID)?;
add_action(&mut svc, Arc::new(get_caps))?;
// GetCurrentTransportActions (passthrough)
let mut get_actions = Action::new("GetCurrentTransportActions".to_string());
add_arg_in(&mut get_actions, "InstanceID", &AVT_INSTANCE_ID)?;
add_action(&mut svc, Arc::new(get_actions))?;
@@ -296,7 +257,6 @@ impl WebRendererFactory {
Ok(svc)
}
/// Construit le service RenderingControl
fn build_renderingcontrol(state: SharedState) -> Result<Service, FactoryError> {
let mut svc = Service::new("RenderingControl".to_string());
@@ -305,7 +265,6 @@ impl WebRendererFactory {
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)?;
@@ -313,7 +272,6 @@ impl WebRendererFactory {
set_vol.set_handler(handlers::set_volume_handler(state.clone()));
add_action(&mut svc, Arc::new(set_vol))?;
// GetVolume
let mut get_vol = Action::new("GetVolume".to_string());
add_arg_in(&mut get_vol, "InstanceID", &RC_INSTANCE_ID)?;
add_arg_in(&mut get_vol, "Channel", &A_ARG_TYPE_CHANNEL)?;
@@ -322,7 +280,6 @@ impl WebRendererFactory {
get_vol.set_handler(handlers::get_volume_handler(state.clone()));
add_action(&mut svc, Arc::new(get_vol))?;
// SetMute
let mut set_mute = Action::new("SetMute".to_string());
add_arg_in(&mut set_mute, "InstanceID", &RC_INSTANCE_ID)?;
add_arg_in(&mut set_mute, "Channel", &A_ARG_TYPE_CHANNEL)?;
@@ -330,7 +287,6 @@ impl WebRendererFactory {
set_mute.set_handler(handlers::set_mute_handler(state.clone()));
add_action(&mut svc, Arc::new(set_mute))?;
// GetMute
let mut get_mute = Action::new("GetMute".to_string());
add_arg_in(&mut get_mute, "InstanceID", &RC_INSTANCE_ID)?;
add_arg_in(&mut get_mute, "Channel", &A_ARG_TYPE_CHANNEL)?;
@@ -342,7 +298,6 @@ impl WebRendererFactory {
Ok(svc)
}
/// Construit le service ConnectionManager
fn build_connectionmanager() -> Result<Service, FactoryError> {
let mut svc = Service::new("ConnectionManager".to_string());
@@ -356,7 +311,6 @@ impl WebRendererFactory {
add_var(&mut svc, &SINKPROTOCOLINFO)?;
add_var(&mut svc, &SOURCEPROTOCOLINFO)?;
// GetProtocolInfo
let mut get_proto = Action::new("GetProtocolInfo".to_string());
add_arg_out(&mut get_proto, "Source", &SOURCEPROTOCOLINFO)?;
add_arg_out(&mut get_proto, "Sink", &SINKPROTOCOLINFO)?;
@@ -364,12 +318,10 @@ impl WebRendererFactory {
get_proto.set_handler(handlers::get_protocol_info_handler());
add_action(&mut svc, Arc::new(get_proto))?;
// GetCurrentConnectionIDs
let mut get_ids = Action::new("GetCurrentConnectionIDs".to_string());
add_arg_out(&mut get_ids, "ConnectionIDs", &CURRENTCONNECTIONIDS)?;
add_action(&mut svc, Arc::new(get_ids))?;
// GetCurrentConnectionInfo
let mut get_conn = Action::new("GetCurrentConnectionInfo".to_string());
add_arg_in(&mut get_conn, "ConnectionID", &A_ARG_TYPE_CONNECTIONID)?;
add_arg_out(&mut get_conn, "RcsID", &A_ARG_TYPE_RCSID)?;

View File

@@ -4,18 +4,11 @@ version = "0.1.0"
edition = "2024"
[dependencies]
pmoupnp = { path = "../pmoupnp" }
pmomediarenderer = { path = "../pmomediarenderer" }
pmoserver = { path = "../pmoserver", optional = true }
pmocontrol = { path = "../pmocontrol", optional = true }
pmoconfig = { path = "../pmoconfig" }
# Audio pipeline (toujours actif — c'est le cœur du renderer)
pmoaudio-ext = { path = "../pmoaudio-ext", features = ["http-stream"] }
pmoaudio = { path = "../pmoaudio" }
pmoflac = { path = "../pmoflac" }
pmometadata = { path = "../pmometadata" }
# Async runtime
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true }
@@ -26,25 +19,16 @@ 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 }
# Utilities
uuid = { workspace = true, features = ["v4", "serde"] }
parking_lot = "0.12"
thiserror = { workspace = true }
tracing = { workspace = true }
pmodidl = { path = "../pmodidl" }
pmoutils = { version = "0.1.2", registry = "pmo" }
[features]
default = []
pmoserver = ["dep:pmoserver", "dep:pmocontrol"]
pmoserver = ["dep:pmoserver", "dep:pmocontrol", "pmomediarenderer/pmoserver"]

View File

@@ -1,44 +1,13 @@
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DeviceCommand {
Stream { url: String },
Play,
Pause,
Seek { position_sec: f64 },
Flush,
Stop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DevicePlaybackState {
Playing,
Paused,
Stopped,
Buffering,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceStateReport {
pub position_sec: Option<f64>,
pub duration_sec: Option<f64>,
pub playback_state: Option<DevicePlaybackState>,
}
pub trait DeviceAdapter: Send + Sync + 'static {
fn deliver(&self, command: DeviceCommand);
fn poll_state(&self) -> Option<DeviceStateReport>;
}
use pmomediarenderer::{
DeviceAdapter, DeviceCommand, DeviceStateReport, SharedState,
};
pub struct BrowserAdapter {
pub state: crate::state::SharedState,
pub state: SharedState,
}
impl BrowserAdapter {
pub fn new(state: crate::state::SharedState) -> Self {
pub fn new(state: SharedState) -> Self {
Self { state }
}
}

View File

@@ -16,14 +16,14 @@ use axum::{
use pmocontrol::ControlPoint;
#[cfg(feature = "pmoserver")]
use crate::error::WebRendererError;
use pmomediarenderer::MediaRendererError;
#[cfg(feature = "pmoserver")]
use crate::register::{
nowplaying_handler, pause_handler, play_handler, position_update_handler,
register_handler, report_handler, set_uri_handler, state_handler, unregister_handler,
};
#[cfg(feature = "pmoserver")]
use crate::registry::RendererRegistry;
use pmomediarenderer::MediaRendererRegistry;
#[cfg(feature = "pmoserver")]
use crate::stream::stream_handler;
@@ -34,7 +34,7 @@ pub trait WebRendererExt {
async fn register_web_renderer(
&mut self,
control_point: Arc<ControlPoint>,
) -> Result<(), WebRendererError>;
) -> Result<(), MediaRendererError>;
}
#[cfg(feature = "pmoserver")]
@@ -43,8 +43,8 @@ impl WebRendererExt for pmoserver::Server {
async fn register_web_renderer(
&mut self,
control_point: Arc<ControlPoint>,
) -> Result<(), WebRendererError> {
let registry = Arc::new(RendererRegistry::new(control_point));
) -> Result<(), MediaRendererError> {
let registry = Arc::new(MediaRendererRegistry::new(control_point));
// POST /api/webrenderer/register
self.add_post_handler_with_state(

View File

@@ -6,26 +6,13 @@
//! - Les commandes UPnP sont relayées vers le pipeline audio via PipelineControl
mod adapter;
mod error;
mod handlers;
mod messages;
mod pipeline;
mod register;
mod registry;
mod renderer;
mod state;
mod stream;
#[cfg(feature = "pmoserver")]
mod config;
pub use adapter::{BrowserAdapter, DeviceAdapter, DeviceCommand, DevicePlaybackState, DeviceStateReport};
pub use error::WebRendererError;
pub use messages::PlaybackState;
pub use pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time};
pub use registry::{RendererRegistry, WebRendererInstance};
pub use renderer::{FactoryError, WebRendererFactory};
pub use state::{RendererState, SharedState};
pub use adapter::BrowserAdapter;
#[cfg(feature = "pmoserver")]
pub use config::WebRendererExt;

View File

@@ -1,12 +0,0 @@
//! Types de messages pour le WebRenderer
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PlaybackState {
Stopped,
Playing,
Paused,
Transitioning,
}

View File

@@ -12,9 +12,11 @@ use axum::{
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::messages::PlaybackState;
use crate::pipeline::PipelineControl;
use crate::registry::RendererRegistry;
use pmomediarenderer::PlaybackState;
use pmomediarenderer::PipelineControl;
use pmomediarenderer::{DeviceAdapter, DeviceCommand, MediaRendererRegistry, PlayerStateReport, SharedState};
use crate::adapter::BrowserAdapter;
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
@@ -26,14 +28,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>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Json(req): Json<RegisterRequest>,
) -> impl IntoResponse {
tracing::info!(
@@ -42,8 +42,13 @@ pub async fn register_handler(
"WebRenderer: register request"
);
let state: SharedState = Arc::new(parking_lot::RwLock::new(
pmomediarenderer::RendererState::default()
));
let adapter: Arc<dyn DeviceAdapter> = Arc::new(BrowserAdapter::new(state));
match registry
.register_or_reconnect(&req.instance_id, &req.user_agent)
.register_or_reconnect(&req.instance_id, &req.user_agent, adapter)
.await
{
Ok((stream_url, udn, should_play)) => {
@@ -73,12 +78,9 @@ pub struct PositionUpdateRequest {
pub duration_sec: Option<f64>,
}
/// 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>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
Json(req): Json<PositionUpdateRequest>,
) -> impl IntoResponse {
@@ -86,10 +88,9 @@ pub async fn position_update_handler(
StatusCode::NO_CONTENT
}
/// DELETE /api/webrenderer/{id}
#[axum::debug_handler]
pub async fn unregister_handler(
State(registry): State<Arc<RendererRegistry>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
tracing::info!(instance_id = %instance_id, "WebRenderer: explicit unregister");
@@ -102,10 +103,9 @@ 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>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
Json(req): Json<UriRequest>,
) -> impl IntoResponse {
@@ -117,48 +117,31 @@ pub async fn set_uri_handler(
StatusCode::OK
}
/// POST /api/webrenderer/{id}/pause
#[axum::debug_handler]
pub async fn pause_handler(
State(registry): State<Arc<RendererRegistry>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
tracing::info!(instance_id = %instance_id, "WebRenderer: pause request");
if let Some(instance) = registry.get_instance(instance_id.as_str()) {
instance.adapter.deliver(crate::adapter::DeviceCommand::Pause);
instance.adapter.deliver(DeviceCommand::Pause);
}
StatusCode::OK
}
// ─── Rapports du player ─────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct PlayerStateReport {
pub position_sec: Option<f64>,
pub duration_sec: Option<f64>,
pub state: Option<String>,
pub ready_state: Option<String>,
}
/// POST /api/webrenderer/{id}/report - recoit rapports position/state du player
#[axum::debug_handler]
pub async fn report_handler(
State(registry): State<Arc<RendererRegistry>>,
State(registry): State<Arc<MediaRendererRegistry>>,
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>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
match registry.get_pending_command(&instance_id).await {
@@ -167,13 +150,11 @@ pub async fn command_handler(
}
}
/// POST /api/webrenderer/{id}/play - tell player to stream and play
#[axum::debug_handler]
pub async fn play_handler(
State(registry): State<Arc<RendererRegistry>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
// Check if there's a valid URI loaded - if not, ignore the play command
let instance = match registry.get_instance(&instance_id) {
Some(i) => i,
None => {
@@ -191,18 +172,14 @@ pub async fn play_handler(
tracing::info!(instance_id = %instance_id, "WebRenderer: play request");
// Send Stream command to the adapter (via pending_commands queue)
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
instance.adapter.deliver(crate::adapter::DeviceCommand::Stream { url: stream_url });
instance.adapter.deliver(DeviceCommand::Stream { url: stream_url });
// Also tell pipeline to play (if not already)
instance.pipeline.send(PipelineControl::Play).await;
(StatusCode::OK, "OK").into_response()
}
// ─── Metadata endpoints ─────────────────────────────────────────────
#[derive(Debug, Serialize)]
pub struct NowPlayingResponse {
pub state: String,
@@ -216,7 +193,7 @@ pub struct NowPlayingResponse {
#[axum::debug_handler]
pub async fn nowplaying_handler(
State(registry): State<Arc<RendererRegistry>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
let state = match registry.get_state(&instance_id) {
@@ -258,7 +235,7 @@ pub struct RendererStateResponse {
#[axum::debug_handler]
pub async fn state_handler(
State(registry): State<Arc<RendererRegistry>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
) -> impl IntoResponse {
let (state, udn) = match registry.get_state_and_udn(&instance_id) {

View File

@@ -18,11 +18,11 @@ use std::sync::Arc;
use tokio_util::io::ReaderStream;
use tracing::{error, info};
use crate::registry::RendererRegistry;
use pmomediarenderer::MediaRendererRegistry;
/// GET /api/webrenderer/{id}/stream
pub async fn stream_handler(
State(registry): State<Arc<RendererRegistry>>,
State(registry): State<Arc<MediaRendererRegistry>>,
Path(instance_id): Path<String>,
headers: HeaderMap,
) -> impl IntoResponse {