Implémentation de la reconnexion stable avec synchronisation d'état

Cette mise à jour permet une reconnexion stable du renderer Web après un reload de page, en conservant l'état audio (URI, volume, lecture en cours). 

- Ajout d'un identifiant d'instance stable (UUID) dans le navigateur pour retrouver le même renderer UPnP
- Implémentation d'un mécanisme de synchronisation d'état (StateSync) lors des reconnexions
- Mise à jour du système de sender partagé (SharedSender) pour permettre le remplacement des connexions WebSocket
- Correction des handlers UPnP pour utiliser le nouveau type SharedSender
- Amélioration de la gestion des erreurs d'autoplay dans le moteur audio
- Mise à jour du numéro de version vers 0.3.21
This commit is contained in:
2026-02-21 20:54:22 +01:00
parent 32464c93fb
commit a502e4fc3e
9 changed files with 373 additions and 106 deletions

View File

@@ -4,7 +4,6 @@
//! envoyée au navigateur, ou lit l'état partagé pour les requêtes GET.
use std::sync::Arc;
use tokio::sync::mpsc;
use pmodidl::DIDLLite;
use pmoupnp::actions::{ActionData, ActionError, ActionHandler, get_value};
@@ -12,19 +11,19 @@ use pmoupnp::{get, set};
use pmoutils::ToXmlElement;
use crate::messages::{CommandParams, PlaybackState, ServerMessage, TransportAction};
use crate::state::SharedState;
use crate::state::{SharedSender, SharedState};
type ActionFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<ActionData, ActionError>> + Send>>;
// ─── AVTransport Handlers ───────────────────────────────────────────────────
pub fn play_handler(ws: mpsc::UnboundedSender<ServerMessage>, state: SharedState) -> ActionHandler {
pub fn play_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let state = state.clone();
Box::pin(async move {
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::Play,
params: None,
});
@@ -34,12 +33,12 @@ pub fn play_handler(ws: mpsc::UnboundedSender<ServerMessage>, state: SharedState
})
}
pub fn stop_handler(ws: mpsc::UnboundedSender<ServerMessage>, state: SharedState) -> ActionHandler {
pub fn stop_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let state = state.clone();
Box::pin(async move {
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::Stop,
params: None,
});
@@ -49,15 +48,12 @@ pub fn stop_handler(ws: mpsc::UnboundedSender<ServerMessage>, state: SharedState
})
}
pub fn pause_handler(
ws: mpsc::UnboundedSender<ServerMessage>,
state: SharedState,
) -> ActionHandler {
pub fn pause_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let state = state.clone();
Box::pin(async move {
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::Pause,
params: None,
});
@@ -67,11 +63,11 @@ pub fn pause_handler(
})
}
pub fn next_handler(ws: mpsc::UnboundedSender<ServerMessage>) -> ActionHandler {
pub fn next_handler(ws: SharedSender) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
Box::pin(async move {
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::Play,
params: None,
});
@@ -80,11 +76,11 @@ pub fn next_handler(ws: mpsc::UnboundedSender<ServerMessage>) -> ActionHandler {
})
}
pub fn previous_handler(ws: mpsc::UnboundedSender<ServerMessage>) -> ActionHandler {
pub fn previous_handler(ws: SharedSender) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
Box::pin(async move {
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::Play,
params: None,
});
@@ -93,12 +89,12 @@ pub fn previous_handler(ws: mpsc::UnboundedSender<ServerMessage>) -> ActionHandl
})
}
pub fn seek_handler(ws: mpsc::UnboundedSender<ServerMessage>) -> ActionHandler {
pub fn seek_handler(ws: SharedSender) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
Box::pin(async move {
let target: String = get!(&data, "Target", String);
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::Seek,
params: Some(CommandParams {
uri: None,
@@ -111,23 +107,19 @@ pub fn seek_handler(ws: mpsc::UnboundedSender<ServerMessage>) -> ActionHandler {
})
}
pub fn set_uri_handler(
ws: mpsc::UnboundedSender<ServerMessage>,
state: SharedState,
) -> ActionHandler {
pub fn set_uri_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let state = state.clone();
Box::pin(async move {
let uri: String = get!(&data, "CurrentURI", String);
// CurrentURIMetaData peut être String ou DIDLLite (après parsing)
let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
.or_else(|_| {
get_value::<DIDLLite>(&data, "CurrentURIMetaData")
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::SetUri,
params: Some(CommandParams {
uri: Some(uri.clone()),
@@ -146,10 +138,7 @@ pub fn set_uri_handler(
})
}
pub fn set_next_uri_handler(
ws: mpsc::UnboundedSender<ServerMessage>,
state: SharedState,
) -> ActionHandler {
pub fn set_next_uri_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let state = state.clone();
@@ -161,7 +150,7 @@ pub fn set_next_uri_handler(
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
let _ = ws.send(ServerMessage::Command {
ws.send(ServerMessage::Command {
action: TransportAction::SetNextUri,
params: Some(CommandParams {
uri: Some(uri.clone()),
@@ -283,16 +272,13 @@ pub fn get_media_info_handler(state: SharedState) -> ActionHandler {
// ─── RenderingControl Handlers ──────────────────────────────────────────────
pub fn set_volume_handler(
ws: mpsc::UnboundedSender<ServerMessage>,
state: SharedState,
) -> ActionHandler {
pub fn set_volume_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let state = state.clone();
Box::pin(async move {
let volume: u16 = get!(&data, "DesiredVolume", u16);
let _ = ws.send(ServerMessage::SetVolume { volume });
ws.send(ServerMessage::SetVolume { volume });
state.write().volume = volume;
Ok(data)
})
@@ -311,16 +297,13 @@ pub fn get_volume_handler(state: SharedState) -> ActionHandler {
})
}
pub fn set_mute_handler(
ws: mpsc::UnboundedSender<ServerMessage>,
state: SharedState,
) -> ActionHandler {
pub fn set_mute_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let state = state.clone();
Box::pin(async move {
let mute: bool = get!(&data, "DesiredMute", bool);
let _ = ws.send(ServerMessage::SetMute { mute });
ws.send(ServerMessage::SetMute { mute });
state.write().mute = mute;
Ok(data)
})

View File

@@ -10,6 +10,18 @@ pub enum ServerMessage {
token: String,
renderer_info: RendererInfo,
},
/// Envoyé après SessionCreated lors d'une reconnexion pour resynchroniser
/// l'état audio du navigateur (URI courante, état de lecture, etc.).
StateSync {
current_uri: Option<String>,
current_metadata: Option<String>,
next_uri: Option<String>,
next_metadata: Option<String>,
playback_state: PlaybackState,
position: Option<String>,
volume: u16,
mute: bool,
},
Command {
action: TransportAction,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -62,6 +74,9 @@ pub enum ClientMessage {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserCapabilities {
/// Identifiant stable de l'instance navigateur (UUID stocké en localStorage).
/// Permet de réutiliser le même renderer UPnP après un reload de page.
pub instance_id: String,
pub user_agent: String,
pub supported_formats: Vec<String>,
}

View File

@@ -13,7 +13,7 @@ use pmoupnp::services::Service;
use crate::handlers;
use crate::messages::ServerMessage;
use crate::state::SharedState;
use crate::state::{SharedSender, SharedState};
// ─── Réimport des variables statiques de pmomediarenderer ───────────────────
// Variables AVTransport
@@ -71,20 +71,25 @@ pub struct WebRendererFactory;
impl WebRendererFactory {
/// Crée un Device model UPnP complet pour un WebRenderer.
///
/// Le device est construit avec des action handlers qui relaient
/// les commandes SOAP vers le navigateur via le `ws_sender`.
pub fn create_device(
browser_name: &str,
/// `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é).
///
/// Retourne le Device et le `SharedSender` associé. Le `SharedSender` peut être
/// mis à jour à chaque reconnexion WebSocket via `shared_sender.set(new_tx)`.
pub fn create_device_with_name(
device_name: &str,
browser_ua: &str,
ws_sender: mpsc::UnboundedSender<ServerMessage>,
state: SharedState,
) -> Result<Device, FactoryError> {
let avtransport = Self::build_avtransport(ws_sender.clone(), state.clone())?;
let renderingcontrol = Self::build_renderingcontrol(ws_sender.clone(), state.clone())?;
) -> Result<(Device, SharedSender), FactoryError> {
let shared_sender = SharedSender::new(ws_sender);
let avtransport = Self::build_avtransport(shared_sender.clone(), state.clone())?;
let renderingcontrol = Self::build_renderingcontrol(shared_sender.clone(), state.clone())?;
let connectionmanager = Self::build_connectionmanager()?;
let short_name = extract_browser_name(browser_name);
let short_name = extract_browser_name(browser_ua);
let device = Device::new(
"WebRenderer".to_string(),
device_name.to_string(),
"MediaRenderer".to_string(),
format!("Web Audio {}", short_name),
);
@@ -98,12 +103,12 @@ impl WebRendererFactory {
.add_service(Arc::new(connectionmanager))
.map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?;
Ok(device)
Ok((device, shared_sender))
}
/// Construit le service AVTransport avec les handlers WebSocket
fn build_avtransport(
ws: mpsc::UnboundedSender<ServerMessage>,
ws: SharedSender,
state: SharedState,
) -> Result<Service, FactoryError> {
let mut svc = Service::new("AVTransport".to_string());
@@ -419,7 +424,7 @@ impl WebRendererFactory {
/// Construit le service RenderingControl avec les handlers WebSocket
fn build_renderingcontrol(
ws: mpsc::UnboundedSender<ServerMessage>,
ws: SharedSender,
state: SharedState,
) -> Result<Service, FactoryError> {
let mut svc = Service::new("RenderingControl".to_string());

View File

@@ -4,19 +4,18 @@ use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::mpsc;
use pmoupnp::devices::DeviceInstance;
use crate::messages::ServerMessage;
use crate::state::SharedState;
use crate::state::{SharedSender, SharedState};
/// Session WebSocket liée à un MediaRenderer privé
pub struct WebRendererSession {
pub token: String,
pub udn: String,
pub device_instance: Arc<DeviceInstance>,
pub ws_sender: mpsc::UnboundedSender<ServerMessage>,
/// Sender partagé : mis à jour à chaque reconnexion WebSocket.
pub shared_sender: SharedSender,
pub state: SharedState,
pub created_at: SystemTime,
pub last_activity: Arc<RwLock<SystemTime>>,
@@ -26,6 +25,12 @@ pub struct WebRendererSession {
#[derive(Clone)]
pub struct SessionManager {
sessions: Arc<RwLock<HashMap<String, Arc<WebRendererSession>>>>,
/// Map UDN → SharedSender, persiste même après suppression de la session.
/// Permet de retrouver et mettre à jour le sender à la reconnexion.
senders: Arc<RwLock<HashMap<String, SharedSender>>>,
/// Map UDN → SharedState, persiste même après suppression de la session.
/// Permet de réutiliser l'état partagé avec les handlers du device existant.
states: Arc<RwLock<HashMap<String, SharedState>>>,
timeout_duration: Duration,
}
@@ -33,6 +38,8 @@ impl SessionManager {
pub fn new(timeout_duration: Duration) -> Self {
let manager = Self {
sessions: Arc::new(RwLock::new(HashMap::new())),
senders: Arc::new(RwLock::new(HashMap::new())),
states: Arc::new(RwLock::new(HashMap::new())),
timeout_duration,
};
@@ -42,7 +49,12 @@ impl SessionManager {
pub fn add_session(&self, session: Arc<WebRendererSession>) {
let token = session.token.clone();
let udn = session.udn.clone();
let sender = session.shared_sender.clone();
let state = session.state.clone();
self.sessions.write().insert(token.clone(), session);
self.senders.write().insert(udn.clone(), sender);
self.states.write().insert(udn, state);
tracing::info!(token = %token, "WebRenderer session added");
}
@@ -56,6 +68,22 @@ impl SessionManager {
}
}
/// Retrouve une session par UDN du device (indépendant du token WebSocket).
pub fn get_session_by_udn(&self, udn: &str) -> Option<Arc<WebRendererSession>> {
let sessions = self.sessions.read();
sessions.values().find(|s| s.udn == udn).cloned()
}
/// Retrouve le SharedSender par UDN (persiste même après suppression de session).
pub fn get_sender_by_udn(&self, udn: &str) -> Option<SharedSender> {
self.senders.read().get(udn).cloned()
}
/// Retrouve le SharedState par UDN (persiste même après suppression de session).
pub fn get_state_by_udn(&self, udn: &str) -> Option<SharedState> {
self.states.read().get(udn).cloned()
}
pub fn remove_session(&self, token: &str) -> Option<Arc<WebRendererSession>> {
let session = self.sessions.write().remove(token);
if let Some(ref s) = session {

View File

@@ -2,8 +2,9 @@
use parking_lot::RwLock;
use std::sync::Arc;
use tokio::sync::mpsc;
use crate::messages::PlaybackState;
use crate::messages::{PlaybackState, ServerMessage};
/// État temps-réel du renderer (partagé backend ↔ navigateur)
#[derive(Debug, Clone)]
@@ -37,3 +38,34 @@ impl Default for RendererState {
/// Alias pour l'état partagé
pub type SharedState = Arc<RwLock<RendererState>>;
/// Sender WebSocket partagé et remplaçable entre les reconnexions.
///
/// Les handlers UPnP capturent ce `Arc` à la création du device. À chaque
/// reconnexion WebSocket (reload de page), on remplace le sender interne via
/// `set()`, sans avoir à recréer le device ni ses handlers.
#[derive(Clone)]
pub struct SharedSender(Arc<RwLock<Option<mpsc::UnboundedSender<ServerMessage>>>>);
impl SharedSender {
pub fn new(sender: mpsc::UnboundedSender<ServerMessage>) -> Self {
Self(Arc::new(RwLock::new(Some(sender))))
}
/// Envoie un message au navigateur. Ignore silencieusement si déconnecté.
pub fn send(&self, msg: ServerMessage) {
if let Some(tx) = self.0.read().as_ref() {
let _ = tx.send(msg);
}
}
/// Remplace le sender (appelé à la reconnexion WebSocket).
pub fn set(&self, sender: mpsc::UnboundedSender<ServerMessage>) {
*self.0.write() = Some(sender);
}
/// Retire le sender (appelé à la déconnexion).
pub fn clear(&self) {
*self.0.write() = None;
}
}

View File

@@ -64,6 +64,7 @@ async fn handle_socket(socket: WebSocket, state: WebSocketState) {
});
let mut session_token: Option<String> = None;
#[allow(unused_variables, unused_assignments, unused_mut)]
let mut device_udn: Option<String> = None;
// Boucle de réception des messages du navigateur
@@ -74,7 +75,7 @@ async fn handle_socket(socket: WebSocket, state: WebSocketState) {
match serde_json::from_str::<ClientMessage>(&text) {
Ok(ClientMessage::Init { capabilities }) => {
tracing::info!("WebRenderer Init received, creating renderer...");
// Créer le renderer UPnP pour ce navigateur
// Créer ou reconnecter le renderer UPnP pour ce navigateur.
match create_renderer_for_browser(&capabilities, tx.clone(), &state).await {
Ok(session) => {
tracing::info!("WebRenderer create_renderer_for_browser OK");
@@ -98,8 +99,32 @@ async fn handle_socket(socket: WebSocket, state: WebSocketState) {
},
});
// Si une URI est déjà chargée (reconnexion en cours de lecture),
// envoyer l'état complet pour que le navigateur puisse reprendre.
{
let s = session.state.read();
if s.current_uri.is_some() {
let _ = tx.send(ServerMessage::StateSync {
current_uri: s.current_uri.clone(),
current_metadata: s.current_metadata.clone(),
next_uri: s.next_uri.clone(),
next_metadata: s.next_metadata.clone(),
playback_state: s.playback_state.clone(),
position: s.position.clone(),
volume: s.volume,
mute: s.mute,
});
tracing::info!(
udn = %udn,
state = ?s.playback_state,
"WebRenderer: sent StateSync to reconnected browser"
);
}
}
session_token = Some(token.clone());
device_udn = Some(udn.clone());
#[cfg(feature = "pmoserver")]
{ device_udn = Some(udn.clone()); }
state.session_manager.add_session(session);
@@ -237,7 +262,13 @@ async fn handle_socket(socket: WebSocket, state: WebSocketState) {
}
}
}
Ok(Message::Close(_)) => break,
Ok(Message::Binary(b)) => {
tracing::warn!("WebRenderer received binary message ({} bytes)", b.len());
}
Ok(Message::Close(_)) => {
tracing::info!("WebRenderer WebSocket closed by client");
break;
}
Err(e) => {
tracing::error!("WebSocket error: {}", e);
break;
@@ -247,6 +278,7 @@ async fn handle_socket(socket: WebSocket, state: WebSocketState) {
}
// Cleanup à la déconnexion
tracing::info!("WebRenderer WebSocket handler exiting (session_token={:?})", session_token);
if let Some(token) = session_token {
state.session_manager.remove_session(&token);
}
@@ -263,65 +295,145 @@ async fn handle_socket(socket: WebSocket, state: WebSocketState) {
send_task.abort();
}
/// Crée un DeviceInstance UPnP et l'enregistre pour un navigateur
/// Crée ou reconnecte un DeviceInstance UPnP pour un navigateur.
///
/// - Première connexion : crée le device, l'enregistre, crée la session.
/// - Reconnexion (reload) : retrouve la session existante par UDN, met à jour le
/// `SharedSender` avec le nouveau tx WebSocket (les handlers continuent de fonctionner),
/// et crée une nouvelle session avec un nouveau token.
async fn create_renderer_for_browser(
capabilities: &BrowserCapabilities,
ws_sender: mpsc::UnboundedSender<ServerMessage>,
ws_state: &WebSocketState,
) -> Result<Arc<WebRendererSession>, crate::error::WebRendererError> {
let shared_state: SharedState = Arc::new(RwLock::new(RendererState::default()));
let token = Uuid::new_v4().to_string();
// Construire le Device model avec les handlers WS
tracing::info!("WebRenderer: creating device model...");
let device = WebRendererFactory::create_device(
&capabilities.user_agent,
ws_sender.clone(),
shared_state.clone(),
)
.map_err(|e| crate::error::WebRendererError::DeviceCreationError(e.to_string()))?;
tracing::info!("WebRenderer: device model created");
// Persister l'UDN dérivé de l'instance_id dans la config pour que device_instance.rs
// le retrouve de façon déterministe. La clé ("MediaRenderer", instance_id) est unique
// par onglet/navigateur et stable entre les reloads.
let instance_udn = capabilities.instance_id.clone();
if let Err(e) = pmoconfig::get_config().set_device_udn(
"MediaRenderer",
&instance_udn,
instance_udn.clone(),
) {
tracing::warn!("WebRenderer: failed to persist UDN in config: {:?}", e);
}
let device = Arc::new(device);
// UDN normalisé tel que stocké dans le DEVICE_REGISTRY (sans préfixe "uuid:")
let candidate_udn = instance_udn.to_ascii_lowercase();
// UDN avec préfixe "uuid:" pour le ControlPoint et la session
let full_udn = format!("uuid:{}", candidate_udn);
// ── Reconnexion : session existante par UDN ───────────────────────────────
// Si une session avec ce même UDN existe encore dans le SessionManager, on
// met à jour son SharedSender (les handlers UPnP enverront vers le nouveau WS).
if let Some(existing_session) = ws_state.session_manager.get_session_by_udn(&full_udn) {
tracing::info!(udn = %full_udn, "WebRenderer: reconnecting via existing session");
existing_session.shared_sender.set(ws_sender.clone());
#[cfg(feature = "pmoserver")]
register_with_control_point(&existing_session.device_instance, ws_state)?;
// Nouvelle session avec nouveau token, mais même device/state/sender partagés
let session = Arc::new(WebRendererSession {
token,
udn: full_udn,
device_instance: existing_session.device_instance.clone(),
shared_sender: existing_session.shared_sender.clone(),
state: existing_session.state.clone(),
created_at: existing_session.created_at,
last_activity: existing_session.last_activity.clone(),
});
return Ok(session);
}
// ── Première connexion : création complète ────────────────────────────────
// Enregistrer le device via UpnpServerExt (gère base_url, register_urls, DEVICE_REGISTRY)
// Retourne (DeviceInstance, SharedSender effectif, SharedState effective pour cette session)
#[cfg(feature = "pmoserver")]
let di = {
let (di, shared_sender, shared_state) = {
use pmoupnp::UpnpServerExt;
tracing::info!("WebRenderer: getting server arc...");
tracing::info!("WebRenderer: candidate UDN = {}", candidate_udn);
// Vérifier si un device avec ce même UDN est déjà dans le DEVICE_REGISTRY
// (cas où la session a expiré mais le device est encore enregistré).
let server_arc =
pmoserver::get_server().ok_or(crate::error::WebRendererError::ServerNotAvailable)?;
tracing::info!("WebRenderer: got server arc, acquiring write lock...");
let di = {
let mut server = server_arc.write().await;
tracing::info!("WebRenderer: write lock acquired, registering device...");
server
.register_device(device)
.await
.map_err(|e| crate::error::WebRendererError::RegistrationError(e.to_string()))?
let existing_di = {
let server = server_arc.read().await;
server.get_device(&candidate_udn)
};
tracing::info!("WebRenderer: device registered, registering with ControlPoint...");
// Enregistrer avec le ControlPoint
register_with_control_point(&di, ws_state)?;
tracing::info!("WebRenderer: registered with ControlPoint");
if let Some(di) = existing_di {
tracing::info!(udn = %candidate_udn, "WebRenderer: reusing device from registry (session expired)");
// Mettre à jour le SharedSender de ce device (session supprimée mais device toujours dans registry).
// Le SharedSender et le SharedState sont ceux capturés dans les handlers du di existant.
let effective_sender = if let Some(existing_sender) = ws_state.session_manager.get_sender_by_udn(&full_udn) {
existing_sender.set(ws_sender);
tracing::info!(udn = %full_udn, "WebRenderer: updated SharedSender for reused device");
existing_sender
} else {
// Fallback : ne devrait pas arriver mais on crée un sender neuf
tracing::warn!(udn = %full_udn, "WebRenderer: no SharedSender found for reused device");
let new_state: SharedState = Arc::new(RwLock::new(RendererState::default()));
let (_, new_sender) = WebRendererFactory::create_device_with_name(
&instance_udn, &capabilities.user_agent, ws_sender, new_state.clone(),
).map_err(|e| crate::error::WebRendererError::DeviceCreationError(e.to_string()))?;
new_sender
};
let effective_state = ws_state.session_manager.get_state_by_udn(&full_udn)
.unwrap_or_else(|| Arc::new(RwLock::new(RendererState::default())));
register_with_control_point(&di, ws_state)?;
(di, effective_sender, effective_state)
} else {
// Véritablement première connexion : créer device + state + sender
let new_state: SharedState = Arc::new(RwLock::new(RendererState::default()));
tracing::info!("WebRenderer: creating device model...");
let (device, new_sender) = WebRendererFactory::create_device_with_name(
&instance_udn,
&capabilities.user_agent,
ws_sender,
new_state.clone(),
)
.map_err(|e| crate::error::WebRendererError::DeviceCreationError(e.to_string()))?;
tracing::info!("WebRenderer: device model created");
di
let device = Arc::new(device);
tracing::info!("WebRenderer: registering new device...");
let di = {
let mut server = server_arc.write().await;
server
.register_device(device)
.await
.map_err(|e| crate::error::WebRendererError::RegistrationError(e.to_string()))?
};
tracing::info!("WebRenderer: device registered");
register_with_control_point(&di, ws_state)?;
(di, new_sender, new_state)
}
};
#[cfg(not(feature = "pmoserver"))]
let di = device.create_instance();
// Normaliser l'UDN avec le préfixe "uuid:" pour correspondre au format SSDP
let udn = format!("uuid:{}", di.udn().to_ascii_lowercase());
let (di, shared_sender, shared_state) = {
let new_state: SharedState = Arc::new(RwLock::new(RendererState::default()));
let (device, new_sender) = WebRendererFactory::create_device_with_name(
&instance_udn,
&capabilities.user_agent,
ws_sender,
new_state.clone(),
)
.map_err(|e| crate::error::WebRendererError::DeviceCreationError(e.to_string()))?;
(Arc::new(device).create_instance(), new_sender, new_state)
};
let session = Arc::new(WebRendererSession {
token,
udn,
udn: full_udn,
device_instance: di,
ws_sender,
shared_sender,
state: shared_state,
created_at: SystemTime::now(),
last_activity: Arc::new(RwLock::new(SystemTime::now())),