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:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
|
||||
Reference in New Issue
Block a user