diff --git a/Blackboard/Todo/webrenderer_architecture_evolution.md b/Blackboard/Todo/webrenderer_architecture_evolution.md index 641c704d..b4cacab7 100644 --- a/Blackboard/Todo/webrenderer_architecture_evolution.md +++ b/Blackboard/Todo/webrenderer_architecture_evolution.md @@ -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` | +| `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 +``` diff --git a/Cargo.lock b/Cargo.lock index b975e1a8..e9391931 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/PMOMusic/src/main.rs b/PMOMusic/src/main.rs index bf0570ae..e1f4f650 100644 --- a/PMOMusic/src/main.rs +++ b/PMOMusic/src/main.rs @@ -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> { // 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 diff --git a/pmomediarenderer/Cargo.toml b/pmomediarenderer/Cargo.toml index 5121ef6a..e670a28e 100644 --- a/pmomediarenderer/Cargo.toml +++ b/pmomediarenderer/Cargo.toml @@ -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"] \ No newline at end of file diff --git a/pmomediarenderer/src/adapter.rs b/pmomediarenderer/src/adapter.rs new file mode 100644 index 00000000..cb542241 --- /dev/null +++ b/pmomediarenderer/src/adapter.rs @@ -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, + pub duration_sec: Option, + pub playback_state: Option, +} + +pub trait DeviceAdapter: Send + Sync + 'static { + fn deliver(&self, command: DeviceCommand); + fn poll_state(&self) -> Option; +} diff --git a/pmomediarenderer/src/avtransport/actions/getmediainfo.rs b/pmomediarenderer/src/avtransport/actions/getmediainfo.rs index ffed1d49..5b4d71bc 100644 --- a/pmomediarenderer/src/avtransport/actions/getmediainfo.rs +++ b/pmomediarenderer/src/avtransport/actions/getmediainfo.rs @@ -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; diff --git a/pmomediarenderer/src/avtransport/actions/getpositioninfo.rs b/pmomediarenderer/src/avtransport/actions/getpositioninfo.rs index c7ac9263..aa3e1996 100644 --- a/pmomediarenderer/src/avtransport/actions/getpositioninfo.rs +++ b/pmomediarenderer/src/avtransport/actions/getpositioninfo.rs @@ -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; diff --git a/pmomediarenderer/src/avtransport/actions/setavtransportnexturi.rs b/pmomediarenderer/src/avtransport/actions/setavtransportnexturi.rs index 94df0c87..2f0668e9 100644 --- a/pmomediarenderer/src/avtransport/actions/setavtransportnexturi.rs +++ b/pmomediarenderer/src/avtransport/actions/setavtransportnexturi.rs @@ -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; diff --git a/pmomediarenderer/src/avtransport/actions/setavtransporturi.rs b/pmomediarenderer/src/avtransport/actions/setavtransporturi.rs index 44e77da4..68d26d9c 100644 --- a/pmomediarenderer/src/avtransport/actions/setavtransporturi.rs +++ b/pmomediarenderer/src/avtransport/actions/setavtransporturi.rs @@ -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; diff --git a/pmomediarenderer/src/avtransport/mod.rs b/pmomediarenderer/src/avtransport/mod.rs index d685ce52..8bf3cf6e 100644 --- a/pmomediarenderer/src/avtransport/mod.rs +++ b/pmomediarenderer/src/avtransport/mod.rs @@ -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, diff --git a/pmomediarenderer/src/device.rs b/pmomediarenderer/src/device.rs deleted file mode 100644 index 43a94241..00000000 --- a/pmomediarenderer/src/device.rs +++ /dev/null @@ -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> = 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) -}); diff --git a/pmowebrenderer/src/error.rs b/pmomediarenderer/src/error.rs similarity index 83% rename from pmowebrenderer/src/error.rs rename to pmomediarenderer/src/error.rs index b67ec766..6360d716 100644 --- a/pmowebrenderer/src/error.rs +++ b/pmomediarenderer/src/error.rs @@ -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), diff --git a/pmowebrenderer/src/handlers.rs b/pmomediarenderer/src/handlers.rs similarity index 94% rename from pmowebrenderer/src/handlers.rs rename to pmomediarenderer/src/handlers.rs index f9e048f1..24b8a7e1 100644 --- a/pmowebrenderer/src/handlers.rs +++ b/pmomediarenderer/src/handlers.rs @@ -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::(&data, "CurrentURIMetaData") .or_else(|_| get_value::(&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", diff --git a/pmomediarenderer/src/lib.rs b/pmomediarenderer/src/lib.rs index 308ef1dd..80dfcad9 100644 --- a/pmomediarenderer/src/lib.rs +++ b/pmomediarenderer/src/lib.rs @@ -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}; \ No newline at end of file diff --git a/pmomediarenderer/src/messages.rs b/pmomediarenderer/src/messages.rs new file mode 100644 index 00000000..cab243c3 --- /dev/null +++ b/pmomediarenderer/src/messages.rs @@ -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, + pub duration_sec: Option, + pub state: Option, + pub ready_state: Option, +} diff --git a/pmowebrenderer/src/pipeline.rs b/pmomediarenderer/src/pipeline.rs similarity index 80% rename from pmowebrenderer/src/pipeline.rs rename to pmomediarenderer/src/pipeline.rs index ffa4f418..04e73bf3 100644 --- a/pmowebrenderer/src/pipeline.rs +++ b/pmomediarenderer/src/pipeline.rs @@ -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); } @@ -221,4 +200,4 @@ pub fn upnp_time_to_seconds(t: &str) -> f64 { [s] => *s, _ => 0.0, } -} +} \ No newline at end of file diff --git a/pmowebrenderer/src/registry.rs b/pmomediarenderer/src/registry.rs similarity index 71% rename from pmowebrenderer/src/registry.rs rename to pmomediarenderer/src/registry.rs index 0ac66fab..960c39d5 100644 --- a/pmowebrenderer/src/registry.rs +++ b/pmomediarenderer/src/registry.rs @@ -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, 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, + pub adapter: Arc, } -/// Registre global des instances WebRenderer -pub struct RendererRegistry { - /// Map instance_id → instance - instances: RwLock>>, - /// Map udn → instance (pour retrouver depuis les handlers UPnP) - by_udn: RwLock>>, - /// Tokens d'annulation des unregister différés (instance_id → token) +pub struct MediaRendererRegistry { + instances: RwLock>>, + by_udn: RwLock>>, pending_unregister: RwLock>, #[cfg(feature = "pmoserver")] control_point: Arc, } -impl RendererRegistry { +impl MediaRendererRegistry { #[cfg(feature = "pmoserver")] pub fn new(control_point: Arc) -> 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, + ) -> 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 { 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> { + pub fn get_instance(&self, instance_id: &str) -> Option> { self.instances.read().get(instance_id).cloned() } - /// Retourne le SharedState par instance_id pub fn get_state(&self, instance_id: &str) -> Option { 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 { 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 { 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> { 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) { 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, 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 { - // UDN stable dérivé de l'instance_id + adapter: Arc, + ) -> Result { 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 = - 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, - ) -> 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(()) } -} +} \ No newline at end of file diff --git a/pmowebrenderer/src/renderer.rs b/pmomediarenderer/src/renderer.rs similarity index 85% rename from pmowebrenderer/src/renderer.rs rename to pmomediarenderer/src/renderer.rs index b92796c5..5f4b854a 100644 --- a/pmowebrenderer/src/renderer.rs +++ b/pmomediarenderer/src/renderer.rs @@ -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) -> 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 { @@ -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 { 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 { 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 { 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)?; diff --git a/pmowebrenderer/src/state.rs b/pmomediarenderer/src/state.rs similarity index 100% rename from pmowebrenderer/src/state.rs rename to pmomediarenderer/src/state.rs diff --git a/pmowebrenderer/Cargo.toml b/pmowebrenderer/Cargo.toml index 6661b080..2821033e 100644 --- a/pmowebrenderer/Cargo.toml +++ b/pmowebrenderer/Cargo.toml @@ -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"] diff --git a/pmowebrenderer/src/adapter.rs b/pmowebrenderer/src/adapter.rs index b80004a5..0b8fea0a 100644 --- a/pmowebrenderer/src/adapter.rs +++ b/pmowebrenderer/src/adapter.rs @@ -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, - pub duration_sec: Option, - pub playback_state: Option, -} - -pub trait DeviceAdapter: Send + Sync + 'static { - fn deliver(&self, command: DeviceCommand); - fn poll_state(&self) -> Option; -} +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 } } } diff --git a/pmowebrenderer/src/config.rs b/pmowebrenderer/src/config.rs index ffd03869..6d256bbc 100644 --- a/pmowebrenderer/src/config.rs +++ b/pmowebrenderer/src/config.rs @@ -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, - ) -> 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, - ) -> 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( diff --git a/pmowebrenderer/src/lib.rs b/pmowebrenderer/src/lib.rs index 12e149d5..f1e9e633 100644 --- a/pmowebrenderer/src/lib.rs +++ b/pmowebrenderer/src/lib.rs @@ -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; +pub use config::WebRendererExt; \ No newline at end of file diff --git a/pmowebrenderer/src/messages.rs b/pmowebrenderer/src/messages.rs deleted file mode 100644 index 6ad5e468..00000000 --- a/pmowebrenderer/src/messages.rs +++ /dev/null @@ -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, -} diff --git a/pmowebrenderer/src/register.rs b/pmowebrenderer/src/register.rs index 71104283..cd76078b 100644 --- a/pmowebrenderer/src/register.rs +++ b/pmowebrenderer/src/register.rs @@ -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>, + State(registry): State>, Json(req): Json, ) -> 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 = 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, } -/// 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>, + State(registry): State>, Path(instance_id): Path, Json(req): Json, ) -> 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>, + State(registry): State>, Path(instance_id): Path, ) -> 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>, + State(registry): State>, Path(instance_id): Path, Json(req): Json, ) -> 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>, + State(registry): State>, Path(instance_id): Path, ) -> 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, - pub duration_sec: Option, - pub state: Option, - pub ready_state: Option, -} - -/// POST /api/webrenderer/{id}/report - recoit rapports position/state du player #[axum::debug_handler] pub async fn report_handler( - State(registry): State>, + State(registry): State>, Path(instance_id): Path, Json(report): Json, ) -> 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>, + State(registry): State>, Path(instance_id): Path, ) -> 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>, + State(registry): State>, Path(instance_id): Path, ) -> 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>, + State(registry): State>, Path(instance_id): Path, ) -> 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>, + State(registry): State>, Path(instance_id): Path, ) -> impl IntoResponse { let (state, udn) = match registry.get_state_and_udn(&instance_id) { @@ -285,4 +262,4 @@ pub async fn state_handler( mute: s.mute, }; (StatusCode::OK, Json(response)).into_response() -} +} \ No newline at end of file diff --git a/pmowebrenderer/src/stream.rs b/pmowebrenderer/src/stream.rs index 52a80398..a0511d18 100644 --- a/pmowebrenderer/src/stream.rs +++ b/pmowebrenderer/src/stream.rs @@ -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>, + State(registry): State>, Path(instance_id): Path, headers: HeaderMap, ) -> impl IntoResponse {