Files
pmomusic/Blackboard/Report/stateful_music_renderer.md
Eric Coissac 02cff8e913 Refactoriser MusicRenderer pour un comportement stateful complet
Cette modification refactorise l'architecture pour que chaque `MusicRenderer` gère son propre thread de surveillance (watcher), au lieu de déléguer le polling au `ControlPoint` centralisé. 

- Ajout d'un module `watcher.rs` avec `WatchStrategy`, `WatchedState` et fonctions helper
- Implémentation de `start_watching()` et `stop_watching()` dans `MusicRenderer`
- Centralisation de la gestion du watcher dans le constructeur et les méthodes `has_been_seen_now()`/`mark_as_offline()`
- Suppression du polling central (~140 lignes) dans `control_point.rs`
- Simplification du `registry.rs` avec suppression des appels manuels `start/stop_watching()`
- Correction du bug dans `refresh_device_presence()` pour le traitement offline→online
- Préparation pour le support futur des notifications push (OpenHome, Chromecast)

L'architecture est maintenant plus robuste avec une meilleure encapsulation, cohérence des événements et une gestion automatique du watcher.
2026-01-16 20:52:29 +01:00

230 lines
12 KiB
Markdown

# Rapport : Rendre MusicRenderer complètement stateful
## Résumé
Refactorisation de l'architecture pour que chaque `MusicRenderer` gère son propre thread de surveillance (watcher), au lieu de déléguer le polling au `ControlPoint` centralisé. Cette modification améliore l'encapsulation, la cohérence des événements et prépare le terrain pour le support futur des notifications push (OpenHome, Chromecast).
## Travail effectué
### Phase 1 : Création du module watcher.rs
**Fichier créé** : `pmocontrol/src/music_renderer/watcher.rs`
Nouveau module contenant :
- `WatchStrategy` enum avec trois variantes :
- `Polling { interval_ms: u64 }` - pour UPnP, LinkPlay, Arylic (500ms)
- `Push` - pour support futur des notifications push
- `Hybrid { polling_interval_ms: u64 }` - pour OpenHome et Chromecast
- `WatchedState` struct pour le cache de détection des changements
- Fonctions helper déplacées depuis `control_point.rs` :
- `playback_state_equal()`
- `playback_position_equal()`
- `compute_logical_playback_state()`
- `extract_track_metadata()`
- `parse_hms_to_secs()`
- Tests unitaires pour les fonctions helper
### Phase 2 : Extension de MusicRenderer
**Fichier modifié** : `pmocontrol/src/music_renderer/musicrenderer.rs`
Nouveaux champs ajoutés à la struct `MusicRenderer` :
- `watched_state: Arc<Mutex<WatchedState>>` - cache pour détection des changements
- `watcher_stop_flag: Arc<AtomicBool>` - signal d'arrêt du thread
- `watcher_handle: Arc<Mutex<Option<JoinHandle<()>>>>` - handle du thread watcher
Nouvelles méthodes publiques :
- `start_watching()` - démarre le thread de surveillance (idempotent)
- `stop_watching()` - arrête le thread gracieusement (idempotent)
- `is_watching()` - retourne l'état du watcher
Nouvelles méthodes internes :
- `spawn_watcher_thread()` - crée le thread avec la stratégie appropriée
- `watcher_loop()` - boucle principale de polling
- `poll_and_emit_changes()` - poll le backend et émet les événements
- `handle_state_change()` - logique d'auto-advance (déplacée depuis ControlPoint)
- `emit_event()` - helper pour émettre un événement via le bus
### Phase 3 : Modification du Registry
**Fichier modifié** : `pmocontrol/src/registry.rs`
Ajout des appels `start_watching()` / `stop_watching()` :
- `push_renderer()` : appelle `start_watching()` quand un renderer arrive en ligne ou est créé
- `device_says_byebye()` : appelle `stop_watching()` avant de marquer offline
- `check_timeouts()` : appelle `stop_watching()` avant de marquer offline sur timeout
### Phase 4 : Simplification du ControlPoint
**Fichier modifié** : `pmocontrol/src/control_point.rs`
Suppressions :
- Thread de polling central (~140 lignes)
- Struct `RendererRuntimeSnapshot`
- Méthodes `emit_renderer_event()` et `handle_renderer_event()`
- Fonctions helper déplacées vers `watcher.rs`
### Phase 5 : Mise à jour du module
**Fichier modifié** : `pmocontrol/src/music_renderer/mod.rs`
Ajout de `pub mod watcher;` pour exposer le nouveau module.
## Liste des fichiers
### Fichiers créés
| Fichier | Description |
|---------|-------------|
| `pmocontrol/src/music_renderer/watcher.rs` | Module watcher avec WatchStrategy, WatchedState et fonctions helper |
### Fichiers modifiés
| Fichier | Modification |
|---------|--------------|
| `pmocontrol/src/music_renderer/musicrenderer.rs` | Ajout champs watcher, méthodes start/stop_watching, logique auto-advance |
| `pmocontrol/src/music_renderer/mod.rs` | Ajout `pub mod watcher;` |
| `pmocontrol/src/registry.rs` | Appels start/stop_watching dans push_renderer, device_says_byebye, check_timeouts |
| `pmocontrol/src/control_point.rs` | Suppression polling central, RendererRuntimeSnapshot, handle_renderer_event, fonctions helper |
## Notes techniques
- Le signal d'arrêt utilise `AtomicBool` avec `Ordering::SeqCst` pour garantir la visibilité entre threads
- Les méthodes `start_watching()` et `stop_watching()` sont idempotentes
- Le thread watcher est nommé `watcher-{friendly_name}` pour faciliter le debug
- L'intervalle de polling est de 500ms (volume/mute toutes les 2 ticks = 1s)
- La logique `compute_logical_playback_state()` compense les bugs des devices Arylic/LinkPlay
- L'auto-advance est maintenant géré directement dans le watcher du MusicRenderer
## Round 2 : Vérification transition offline → online
### Problème identifié
La méthode `refresh_device_presence()` dans `registry.rs` n'appelait pas `start_watching()` quand un renderer passait de offline à online. Cette méthode est appelée lors de la réception de messages SSDP Alive.
### Correction appliquée
**Fichier modifié** : `pmocontrol/src/registry.rs`
Ajout de l'appel `renderer.start_watching()` dans `refresh_device_presence()` quand `was_online == false`.
### Points de démarrage du watcher vérifiés
| Méthode | Situation | `start_watching()` appelé |
|---------|-----------|---------------------------|
| `push_renderer()` | Nouveau renderer | Oui |
| `push_renderer()` | Renderer existant, était offline | Oui |
| `refresh_device_presence()` | Renderer existant, était offline | Oui (corrigé) |
### Points d'arrêt du watcher vérifiés
| Méthode | Situation | `stop_watching()` appelé |
|---------|-----------|--------------------------|
| `device_says_byebye()` | SSDP ByeBye reçu | Oui |
| `check_timeouts()` | Timeout dépassé | Oui |
## Round 3 : Audit complet de la logique offline/online
Suite à la découverte du manque dans le Round 2, un audit complet de tous les chemins offline/online a été effectué.
### Chemins qui appellent `start_watching()`
| Chemin | Fonction | Ligne | Condition | Status |
|--------|----------|-------|-----------|--------|
| Nouveau renderer découvert | `push_renderer()` | 180, 194 | Création nouvelle entry | ✅ OK |
| Renderer existant, ajout renderer à entry | `push_renderer()` | 169 | Entry existe sans renderer | ✅ OK |
| Renderer existant revient online | `push_renderer()` | 160 | `!was_online` | ✅ OK |
| SSDP Alive pour device connu | `refresh_device_presence()` | 269 | `!was_online` | ✅ OK (corrigé Round 2) |
### Chemins qui appellent `stop_watching()`
| Chemin | Fonction | Ligne | Condition | Status |
|--------|----------|-------|-----------|--------|
| SSDP ByeBye reçu | `device_says_byebye()` | 289 | Renderer présent | ✅ OK |
| Timeout dépassé | `check_timeouts()` | 308 | `elapsed > max_age` | ✅ OK |
### Analyse des flux
```
┌─────────────────────────────────────────────────────────────────┐
│ FLUX ONLINE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SSDP Discovery ──► push_renderer() ──► start_watching() ✅ │
│ │
│ SSDP Alive (nouveau UDN) ──► push_renderer() ──► start_watching() ✅ │
│ │
│ SSDP Alive (UDN connu, online) ──► refresh_device_presence() │
│ (pas de start car déjà en marche) │
│ │
│ SSDP Alive (UDN connu, offline) ──► refresh_device_presence() │
│ ──► start_watching() ✅ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ FLUX OFFLINE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ SSDP ByeBye ──► device_says_byebye() ──► stop_watching() ✅ │
│ │
│ Timeout ──► check_timeouts() ──► stop_watching() ✅ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### Conclusion
**Tous les chemins sont correctement gérés.** Chaque transition offline→online appelle `start_watching()` et chaque transition online→offline appelle `stop_watching()`.
L'idempotence des méthodes `start_watching()` et `stop_watching()` garantit qu'aucun problème ne survient en cas d'appels multiples.
## Round 4 : Centralisation de la gestion du watcher
### Problème identifié
Les appels à `start_watching()` et `stop_watching()` étaient dispersés dans `registry.rs` (6 emplacements), augmentant le risque d'oubli (comme découvert en Round 2).
### Solution implémentée
Centralisation de la gestion du watcher dans `MusicRenderer` lui-même :
1. **Constructeur** (`from_renderer_info_with_bus()`) : appelle automatiquement `start_watching()` à la fin, car le renderer est créé avec `online = true`
2. **`has_been_seen_now()`** : appelle automatiquement `start_watching()` si transition offline→online
3. **`mark_as_offline()`** : appelle automatiquement `stop_watching()` avant de passer offline
### Fichiers modifiés
| Fichier | Modification |
|---------|--------------|
| `pmocontrol/src/music_renderer/musicrenderer.rs` | Ajout `start_watching()` dans constructeur, dans `has_been_seen_now()` et `stop_watching()` dans `mark_as_offline()` |
| `pmocontrol/src/registry.rs` | Suppression de tous les appels manuels à `start_watching()` et `stop_watching()` |
### Avantages
- **Encapsulation** : la logique watcher est entièrement gérée par `MusicRenderer`
- **Impossible d'oublier** : les transitions sont automatiquement gérées
- **Code simplifié** : `registry.rs` ne contient plus de logique watcher
- **Idempotence** : les appels multiples sont sans effet grâce aux guards existants
### Nouvelle architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ GESTION AUTOMATIQUE DU WATCHER │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Création MusicRenderer ──► constructeur ──► start_watching() │
│ │
│ has_been_seen_now() ──► si !was_online ──► start_watching() │
│ │
│ mark_as_offline() ──► stop_watching() ──► online = false │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Compilation
Le projet compile sans erreur.