Files
pmomusic/pmocontrol/src/registry.rs
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

348 lines
12 KiB
Rust

use std::collections::HashMap;
use std::sync::Arc;
use std::time::SystemTime;
use crate::errors::ControlPointError;
use crate::events::{MediaServerEventBus, RendererEventBus};
use crate::media_server::MusicServer;
use crate::model::RendererInfo;
use crate::music_renderer::MusicRenderer;
use crate::{
DeviceId, DeviceIdentity, DeviceOnline, MediaServerEvent, RendererEvent, UpnpMediaServer,
};
const DEFAULT_MAX_AGE: u32 = 1800;
#[derive(Debug, Clone)]
pub struct DeviceItem {
music_renderer: Option<Arc<MusicRenderer>>,
music_server: Option<Arc<MusicServer>>,
}
pub struct DeviceRegistry {
devices: HashMap<DeviceId, DeviceItem>,
udn_index: HashMap<String, DeviceId>,
renderer_bus: RendererEventBus,
server_bus: MediaServerEventBus,
}
#[derive(Debug, Clone)]
pub enum DeviceUpdate {
OfflineById(DeviceId),
OfflineByUdn(String),
}
impl DeviceItem {
pub fn as_music_renderer(&self) -> Result<Arc<MusicRenderer>, ControlPointError> {
self.music_renderer
.clone() // Clone l'Option<Arc<...>>
.ok_or_else(|| ControlPointError::IsNotAMediaRender(format!("{:#?}", self)))
}
pub fn is_a_music_renderer(&self) -> bool {
self.music_renderer.is_some()
}
pub fn as_music_server(&self) -> Result<Arc<MusicServer>, ControlPointError> {
self.music_server
.clone() // Clone l'Option<Arc<...>>
.ok_or_else(|| ControlPointError::IsNotAMediaRender(format!("{:#?}", self)))
}
pub fn is_a_music_server(&self) -> bool {
self.music_server.is_some()
}
}
impl DeviceOnline for DeviceItem {
fn is_online(&self) -> bool {
self.music_renderer
.as_ref()
.map_or(false, |r| r.is_online())
|| self.music_server.as_ref().map_or(false, |s| s.is_online())
}
fn last_seen(&self) -> SystemTime {
let renderer_time = self.music_renderer.as_ref().map(|r| r.last_seen());
let server_time = self.music_server.as_ref().map(|s| s.last_seen());
match (renderer_time, server_time) {
(Some(r), Some(s)) => r.max(s), // Le plus récent
(Some(r), None) => r,
(None, Some(s)) => s,
(None, None) => unreachable!("DeviceEntry must have at least one component"),
}
}
fn has_been_seen_now(&self, max_age: u32) {
if let Some(r) = &self.music_renderer {
r.has_been_seen_now(max_age);
}
if let Some(s) = &self.music_server {
s.has_been_seen_now(max_age);
}
}
fn mark_as_offline(&self) {
if let Some(r) = &self.music_renderer {
r.mark_as_offline();
}
if let Some(s) = &self.music_server {
s.mark_as_offline();
}
}
fn max_age(&self) -> u32 {
let renderer_max_age = self.music_renderer.as_ref().map(|r| r.max_age());
let server_max_age = self.music_server.as_ref().map(|s| s.max_age());
match (renderer_max_age, server_max_age) {
(Some(r), Some(s)) => r.max(s), // Le plus récent
(Some(r), None) => r,
(None, Some(s)) => s,
(None, None) => unreachable!("DeviceEntry must have at least one component"),
}
}
}
impl DeviceRegistry {
pub fn new(renderer_bus: &RendererEventBus, server_bus: &MediaServerEventBus) -> Self {
Self {
devices: HashMap::new(),
udn_index: HashMap::new(),
renderer_bus: renderer_bus.clone(),
server_bus: server_bus.clone(),
}
}
pub fn list_renderers(&self) -> Result<Vec<Arc<MusicRenderer>>, ControlPointError> {
self.devices
.iter()
.filter(|(_, item)| item.is_a_music_renderer())
.map(|(_, item)| item.as_music_renderer())
.collect()
}
pub fn list_servers(&self) -> Result<Vec<Arc<MusicServer>>, ControlPointError> {
self.devices
.iter()
.filter(|(_, item)| item.is_a_music_server())
.map(|(_, item)| item.as_music_server())
.collect()
}
pub fn get_renderer(&self, id: &DeviceId) -> Option<Arc<MusicRenderer>> {
self.devices.get(id)?.as_music_renderer().ok()
}
pub fn get_server(&self, id: &DeviceId) -> Option<Arc<MusicServer>> {
self.devices.get(id)?.as_music_server().ok()
}
pub fn get_device_by_udn(&self, udn: &str) -> Option<DeviceItem> {
let lookup = udn.to_ascii_lowercase();
self.udn_index
.get(&lookup)
.and_then(|id| self.devices.get(id).cloned())
}
/// Helper: get a renderer by UDN (case-insensitive, via udn_index).
pub fn get_renderer_by_udn(&self, udn: &str) -> Option<Arc<MusicRenderer>> {
self.get_device_by_udn(udn)
.and_then(|item| item.as_music_renderer().ok())
}
/// Helper: get a server by UDN (case-insensitive, via udn_index).
pub fn get_server_by_udn(&self, udn: &str) -> Option<Arc<MusicServer>> {
self.get_device_by_udn(udn)
.and_then(|item| item.as_music_server().ok())
}
pub fn push_renderer(&mut self, info: &RendererInfo, max_age: u32) {
let device_id = info.id();
if let Some(entry) = self.devices.get_mut(&device_id) {
if let Some(renderer) = &entry.music_renderer {
let was_online = renderer.is_online();
// has_been_seen_now() automatically calls start_watching() if offline→online
renderer.has_been_seen_now(max_age);
if !was_online {
self.renderer_bus.broadcast(RendererEvent::Online {
id: device_id.clone(),
info: info.basic_info(),
});
}
return;
}
// Entry existe mais pas de renderer -> on l'ajoute
// Constructor automatically calls start_watching()
if let Ok(new_renderer) =
MusicRenderer::from_renderer_info_with_bus(info, Some(self.renderer_bus.clone()))
{
entry.music_renderer = Some(Arc::new(new_renderer));
self.udn_index
.insert(info.udn().to_ascii_lowercase(), device_id.clone());
self.renderer_bus.broadcast(RendererEvent::Online {
id: device_id.clone(),
info: info.basic_info(),
});
}
} else {
// Entry n'existe pas -> on crée
// Constructor automatically calls start_watching()
if let Ok(new_renderer) =
MusicRenderer::from_renderer_info_with_bus(info, Some(self.renderer_bus.clone()))
{
let new_entry = DeviceItem {
music_renderer: Some(Arc::new(new_renderer)),
music_server: None,
};
self.devices.insert(device_id.clone(), new_entry);
self.udn_index
.insert(info.udn().to_ascii_lowercase(), device_id.clone());
self.renderer_bus.broadcast(RendererEvent::Online {
id: device_id,
info: info.basic_info(),
});
}
}
}
pub fn push_server(&mut self, info: &UpnpMediaServer, max_age: u32) {
let device_id = info.id();
if let Some(entry) = self.devices.get_mut(&device_id) {
if let Some(server) = &entry.music_server {
let was_online = server.is_online();
server.has_been_seen_now(max_age);
if !was_online {
self.server_bus.broadcast(MediaServerEvent::Online {
server_id: device_id.clone(),
info: info.basic_info(),
});
}
return;
}
// Entry existe mais pas de renderer -> on l'ajoute
if let Ok(new_server) = MusicServer::from_server_info(info) {
entry.music_server = Some(Arc::new(new_server));
self.udn_index
.insert(info.udn().to_ascii_lowercase(), device_id.clone());
// Broadcast sur le bon bus
self.server_bus.broadcast(MediaServerEvent::Online {
server_id: device_id.clone(),
info: info.basic_info(),
});
}
} else {
// Entry n'existe pas -> on crée
if let Ok(new_server) = MusicServer::from_server_info(info) {
let new_entry = DeviceItem {
music_renderer: None,
music_server: Some(Arc::new(new_server)),
};
self.devices.insert(device_id.clone(), new_entry);
self.udn_index
.insert(info.udn().to_ascii_lowercase(), device_id.clone());
// Broadcast sur le bon bus
self.server_bus.broadcast(MediaServerEvent::Online {
server_id: device_id,
info: info.basic_info(),
});
}
}
}
/// Updates the last_seen timestamp for a device without fetching its full description.
///
/// This is critical for keeping devices online when SSDP Alive messages arrive
/// more frequently than the UDN cache refresh interval (max_age/2).
///
/// Note: has_been_seen_now() automatically calls start_watching() for renderers
/// when transitioning from offline to online.
pub fn refresh_device_presence(&mut self, udn: &str, max_age: u32) {
let lookup = udn.to_ascii_lowercase();
if let Some(id) = self.udn_index.get(&lookup) {
if let Some(device) = self.devices.get(id) {
let was_online = device.is_online();
// has_been_seen_now() automatically calls start_watching() if offline→online
device.has_been_seen_now(max_age);
// Broadcast Online event if device came back online
if !was_online {
if device.is_a_music_renderer() {
if let Ok(renderer) = device.as_music_renderer() {
self.renderer_bus.broadcast(RendererEvent::Online {
id: id.clone(),
info: renderer.info().basic_info(),
});
}
}
if device.is_a_music_server() {
if let Ok(server) = device.as_music_server() {
self.server_bus.broadcast(MediaServerEvent::Online {
server_id: id.clone(),
info: server.basic_info(),
});
}
}
}
}
}
}
pub fn device_says_byebye(&mut self, udn: &str) {
let lookup = udn.to_ascii_lowercase();
if let Some(id) = self.udn_index.get(&lookup) {
if let Some(device) = self.devices.get(id) {
// mark_as_offline() automatically calls stop_watching() for renderers
device.mark_as_offline();
if device.is_a_music_renderer() {
self.renderer_bus
.broadcast(RendererEvent::Offline { id: id.clone() });
}
if device.is_a_music_server() {
self.server_bus.broadcast(MediaServerEvent::Offline {
server_id: id.clone(),
});
}
}
}
}
pub fn check_timeouts(&mut self) {
let now = SystemTime::now();
for (id, device) in &self.devices {
if let Ok(elapsed) = now.duration_since(device.last_seen()) {
if elapsed.as_secs() > device.max_age() as u64 {
// mark_as_offline() automatically calls stop_watching() for renderers
device.mark_as_offline();
if device.is_a_music_renderer() {
self.renderer_bus
.broadcast(RendererEvent::Offline { id: id.clone() });
}
if device.is_a_music_server() {
self.server_bus.broadcast(MediaServerEvent::Offline {
server_id: id.clone(),
});
}
}
}
}
}
}