🔧 Refactor PMOPlayer and WebRenderer command handling

- Extract beforeunload handler in useWebrenderer.ts to ensure consistent unregister() calls
- Remove unused AudioContext from PMOPlayer and clean up destroy()
  - Add guaranteed final report via sendBeacon during destruction
- Simplify playStream() in PMOPlayer by removing URL rewriting logic (now handled server-side)
- Update play_handler to pass instance_id and store stream command atomically
  - Avoid race conditions when setting current_uri before sending Play control event
- Remove obsolete WebSocket message types from messages.rs (now using HTTP polling)
  - Keep only core PlaybackState enum
- Fix registry::get_player_command to avoid nested locks on HashMap + Sharedstate Arcs  
- Add missing PlaybackState::Transitioning in pipeline.rs TrackEnded event
- Refactor renderer service variable registration using helper add_var() function
This commit is contained in:
2026-04-05 14:32:19 +02:00
parent fe9dd9aba6
commit 76a1c89137
8 changed files with 111 additions and 231 deletions

View File

@@ -4,12 +4,6 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum WebRendererError {
#[error("Session not found: {0}")]
SessionNotFound(String),
#[error("Failed to send message to websocket: {0}")]
WebSocketSendError(String),
#[error("Invalid argument: {0}")]
InvalidArgument(String),

View File

@@ -14,19 +14,25 @@ use crate::state::SharedState;
// ─── AVTransport : commandes de transport ─────────────────────────────────────
pub fn play_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
action_handler!(captures(pipeline, state) |data| {
pub fn play_handler(pipeline: PipelineHandle, state: SharedState, instance_id: String) -> ActionHandler {
action_handler!(captures(pipeline, state, instance_id) |data| {
tracing::info!("[WebRenderer] UPnP Play action invoked");
let has_uri = state.read().current_uri.is_some();
state.write().playback_state = PlaybackState::Transitioning;
let has_uri = {
let mut s = state.write();
let has = s.current_uri.is_some();
s.playback_state = PlaybackState::Transitioning;
if has {
s.player_command = Some(serde_json::json!({
"type": "stream",
"url": format!("/api/webrenderer/{}/stream", instance_id)
}));
tracing::info!("UPnP Play: stored stream command for frontend polling");
}
has
};
if has_uri {
state.write().player_command = Some(serde_json::json!({
"type": "stream",
"url": "/api/webrenderer/stream"
}));
tracing::info!("UPnP Play: stored stream command for frontend polling");
pipeline.send(PipelineControl::Play).await;
}
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
}
@@ -81,14 +87,13 @@ pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHa
.unwrap_or_default();
tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline");
pipeline.send(PipelineControl::LoadUri(uri.clone())).await;
{
let mut s = state.write();
s.current_uri = Some(uri);
s.current_uri = Some(uri.clone());
s.current_metadata = Some(metadata);
s.playback_state = PlaybackState::Transitioning;
}
pipeline.send(PipelineControl::LoadUri(uri)).await;
Ok(data)
})
}
@@ -100,12 +105,12 @@ pub fn set_next_uri_handler(pipeline: PipelineHandle, state: SharedState) -> Act
.or_else(|_| get_value::<DIDLLite>(&data, "NextURIMetaData").map(|didl| didl.to_xml()))
.unwrap_or_default();
pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await;
{
let mut s = state.write();
s.next_uri = Some(uri);
s.next_uri = Some(uri.clone());
s.next_metadata = Some(metadata);
}
pipeline.send(PipelineControl::LoadNextUri(uri)).await;
Ok(data)
})
}

View File

@@ -1,108 +1,7 @@
//! Messages WebSocket pour la communication Backend ↔ Navigateur
//! Types de messages pour le WebRenderer
use serde::{Deserialize, Serialize};
/// Messages envoyés du Backend → Navigateur
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(dead_code)]
pub enum ServerMessage {
SessionCreated {
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")]
params: Option<CommandParams>,
},
SetVolume {
volume: u16,
},
SetMute {
mute: bool,
},
Ping,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[allow(dead_code)]
pub enum TransportAction {
Play,
Pause,
Stop,
Seek,
SetUri,
SetNextUri,
/// Flush buffer immediatement - pour reponse rapide
Flush,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<String>,
}
/// Messages envoyés du Navigateur → Backend
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(dead_code)]
pub enum ClientMessage {
Init {
capabilities: BrowserCapabilities,
},
StateUpdate {
state: PlaybackState,
},
PositionUpdate {
position: String,
duration: String,
},
MetadataUpdate {
metadata: TrackMetadata,
},
VolumeUpdate {
volume: u16,
mute: bool,
},
/// Envoyé quand la piste courante se termine naturellement (gapless).
/// Le backend fait avancer current → next dans l'état partagé.
TrackEnded,
/// Ready state du player HTML5 audio
/// have_nothing, have_metadata, have_current_data, have_future_data, can_play, can_play_through
ReadyStateUpdate {
ready_state: String,
},
Pong,
}
#[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>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PlaybackState {
@@ -111,20 +10,3 @@ pub enum PlaybackState {
Paused,
Transitioning,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackMetadata {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub duration: Option<String>,
pub album_art_uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RendererInfo {
pub udn: String,
pub friendly_name: String,
pub model_name: String,
pub description_url: String,
}

View File

@@ -165,6 +165,7 @@ async fn run_event_listener(
state.write().position = Some(seconds_to_upnp_time(position_sec));
}
PlayerEvent::TrackEnded => {
state.write().playback_state = PlaybackState::Transitioning;
#[cfg(feature = "pmoserver")]
{
let cp = control_point.clone();

View File

@@ -255,10 +255,10 @@ impl RendererRegistry {
&self,
instance_id: &str,
) -> Option<serde_json::Value> {
self.instances
.read()
.get(instance_id)
.and_then(|instance| instance.state.write().player_command.take())
// Extraire le Arc<SharedState> puis relâcher le read lock du HashMap
// avant d'acquérir le write lock sur state (évite double-lock imbriqué).
let state = self.instances.read().get(instance_id).map(|i| i.state.clone())?;
state.write().player_command.take()
}
/// Stocke une commande pour le player (consommée via GET /command)

View File

@@ -117,7 +117,7 @@ impl WebRendererFactory {
pipeline: PipelineHandle,
state: SharedState,
) -> Result<Device, FactoryError> {
let avtransport = Self::build_avtransport(pipeline.clone(), state.clone())?;
let avtransport = Self::build_avtransport(pipeline.clone(), state.clone(), device_name)?;
let renderingcontrol = Self::build_renderingcontrol(state.clone())?;
let connectionmanager = Self::build_connectionmanager()?;
@@ -145,6 +145,7 @@ impl WebRendererFactory {
fn build_avtransport(
pipeline: PipelineHandle,
state: SharedState,
instance_id: &str,
) -> Result<Service, FactoryError> {
let mut svc = Service::new("AVTransport".to_string());
@@ -176,7 +177,7 @@ impl WebRendererFactory {
let mut play = Action::new("Play".to_string());
add_arg_in(&mut play, "InstanceID", &AVT_INSTANCE_ID)?;
add_arg_in(&mut play, "Speed", &TRANSPORTPLAYSPEED)?;
play.set_handler(handlers::play_handler(pipeline.clone(), state.clone()));
play.set_handler(handlers::play_handler(pipeline.clone(), state.clone(), instance_id.to_string()));
add_action(&mut svc, Arc::new(play))?;
// Stop
@@ -345,24 +346,15 @@ impl WebRendererFactory {
fn build_connectionmanager() -> Result<Service, FactoryError> {
let mut svc = Service::new("ConnectionManager".to_string());
svc.add_variable(Arc::clone(&A_ARG_TYPE_CONNECTIONID))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&A_ARG_TYPE_CONNECTIONSTATUS))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&A_ARG_TYPE_DIRECTION))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&A_ARG_TYPE_PROTOCOLINFO))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&A_ARG_TYPE_RCSID))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&A_ARG_TYPE_AVTRANSPORTID))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&CURRENTCONNECTIONIDS))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&SINKPROTOCOLINFO))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
svc.add_variable(Arc::clone(&SOURCEPROTOCOLINFO))
.map_err(|e| FactoryError::VariableError(e.to_string()))?;
add_var(&mut svc, &A_ARG_TYPE_CONNECTIONID)?;
add_var(&mut svc, &A_ARG_TYPE_CONNECTIONSTATUS)?;
add_var(&mut svc, &A_ARG_TYPE_DIRECTION)?;
add_var(&mut svc, &A_ARG_TYPE_PROTOCOLINFO)?;
add_var(&mut svc, &A_ARG_TYPE_RCSID)?;
add_var(&mut svc, &A_ARG_TYPE_AVTRANSPORTID)?;
add_var(&mut svc, &CURRENTCONNECTIONIDS)?;
add_var(&mut svc, &SINKPROTOCOLINFO)?;
add_var(&mut svc, &SOURCEPROTOCOLINFO)?;
// GetProtocolInfo
let mut get_proto = Action::new("GetProtocolInfo".to_string());