Files
pmomusic/Blackboard/Architecture/webrenderer.md
Eric Coissac 1e882ba6c3 feat: implémentation du WebRenderer UPnP privé par navigateur
Ajout de la fonctionnalité WebRenderer permettant à chaque navigateur connecté de devenir un MediaRenderer UPnP privé.

- Création du crate pmowebrenderer avec l'architecture complète
- Implémentation des handlers SOAP → WebSocket pour les services AVTransport, RenderingControl et ConnectionManager
- Intégration avec le ControlPoint pour l'enregistrement dynamique des renderers
- Gestion des sessions avec timeout et cleanup automatique
- Support des commandes de transport (play, pause, stop, seek) et du contrôle du volume
- Mise à jour des dépendances dans Cargo.toml et Cargo.lock
- Documentation de l'architecture dans Blackboard/Architecture/webrenderer.md
2026-02-19 16:15:18 +01:00

5.7 KiB

WebRenderer UPnP privé par navigateur

Vue d'ensemble

Le crate pmowebrenderer transforme chaque navigateur connecté en un MediaRenderer UPnP privé. Quand un navigateur se connecte via WebSocket, le backend Rust crée dynamiquement un device UPnP dédié. Le ControlPoint envoie des commandes SOAP à ce device, et les action handlers les relaient au navigateur via WebSocket. Le navigateur joue l'audio via <audio> et renvoie l'état au backend.

Architecture

Browser (Vue.js)               Rust Backend                    ControlPoint
    |                              |                              |
    |-- WS connect --------------->|                              |
    |<-- SessionCreated (token) ---|                              |
    |-- Init (capabilities) ------>|                              |
    |                              |-- register_device() -------->| (Server)
    |                              |   (Device + Services custom) |
    |                              |-- push_renderer() ---------->| (CP registry)
    |                              |                              |
    |                              |<-- SOAP Play (control_handler)
    |<-- Command(Play, uri) -------|   (action handler -> WS)     |
    |-- StateUpdate(Playing) ----->|                              |
    |                              |-- update StateVarInstance -->| (evented -> SSE)
    |                              |                              |
    |-- WS disconnect ------------>|                              |
    |                              |-- device_says_byebye() ----->| (CP registry)

Flux de connexion

  1. Le navigateur ouvre une WebSocket vers /api/webrenderer/ws
  2. Il envoie un message Init avec ses capabilities (user_agent, formats supportes)
  3. Le backend construit un Device UPnP avec des Service models custom :
    • AVTransport (Play, Stop, Pause, Seek, SetURI, GetPositionInfo, etc.)
    • RenderingControl (SetVolume, GetVolume, SetMute, GetMute)
    • ConnectionManager (GetProtocolInfo)
  4. Chaque Action a un handler qui capture le mpsc::UnboundedSender<ServerMessage> du WS
  5. Le device est enregistre via Server::register_device() (routes SOAP + DEVICE_REGISTRY)
  6. Un RendererInfo est pousse dans le DeviceRegistry du ControlPoint via push_renderer()
  7. Le backend renvoie un SessionCreated avec le token et les infos du renderer

Decision cle : Services dynamiques (zero changement pmoupnp)

Plutot que de modifier pmoupnp pour permettre l'override de handlers post-creation, on construit des Service models dynamiques pour chaque session WebSocket :

  • Les StateVariable statics de pmomediarenderer sont reutilisees via Arc::clone(&*VAR)
  • De nouvelles Action sont creees avec Action::new(), configurees avec set_handler() puis wrappees en Arc
  • Les handlers capturent le sender WS et le SharedState (clone a chaque appel via Fn closure)
  • Le Device est construit avec ces services custom, puis enregistre normalement

Cela reutilise toute l'infrastructure existante sans modification de pmoupnp ni pmomediarenderer.

Propagation d'etat bidirectionnelle

SOAP -> Navigateur (commandes)

Les action handlers des services AVTransport/RenderingControl :

  1. Lisent les arguments SOAP depuis ActionData via la macro get!()
  2. Envoient un ServerMessage::Command ou SetVolume/SetMute via le canal mpsc
  3. Mettent a jour le SharedState local
  4. Retournent les arguments OUT via set!() si necessaire

Navigateur -> UPnP (etats)

Quand le navigateur envoie StateUpdate, PositionUpdate, MetadataUpdate ou VolumeUpdate :

  1. Le SharedState est mis a jour
  2. Les StateVarInstance du DeviceInstance sont mises a jour via set_value(StateValue::...)
  3. Les variables evented declenchent les notifications UPnP captees par le watcher du ControlPoint

Cycle de vie

  • Connexion : creation du Device, enregistrement aupres du Server et du ControlPoint
  • Session active : le SessionManager gere un timeout de 30 minutes d'inactivite
  • Deconnexion WS : appel a device_says_byebye() sur le registry du ControlPoint pour marquer offline
  • Cleanup automatique : le SessionManager verifie toutes les 60 secondes les sessions expirees
  • Pas de SSDP : les WebRenderers sont injectes directement, max_age de 86400s

Structure des fichiers

Fichier Role
handlers.rs Action handlers SOAP->WS (play, stop, pause, seek, set_uri, get_*_info, volume, mute)
renderer.rs WebRendererFactory : construction dynamique Device/Services avec handlers
websocket.rs Handler WS : connexion, reception messages, creation device, propagation etat
session.rs SessionManager : gestion des sessions avec timeout
config.rs WebRendererExt trait pour pmoserver::Server (enregistrement route WS)
messages.rs Types de messages WS (ServerMessage, ClientMessage, etc.)
state.rs RendererState et SharedState (etat partage entre handlers et WS)
error.rs Types d'erreur du crate
lib.rs Exports publics

Points d'attention

  1. parking_lot::RwLockWriteGuard non-Send : les guards de SharedState doivent etre dropes avant tout .await dans les handlers async. Utiliser des blocs { ... } pour limiter la portee.

  2. Fn vs FnOnce : les ActionHandler sont Fn (appeles plusieurs fois). Les closures doivent cloner ws et state a chaque appel, avant le async move.

  3. Routes Axum persistantes : Axum ne supporte pas la suppression de routes. Les routes SOAP d'un device deconnecte persistent mais les handlers retournent des erreurs naturellement.

  4. Acces au Server : le WebSocket handler utilise pmoserver::get_server() (singleton global) pour enregistrer les devices dynamiquement.