diff --git a/Cargo.lock b/Cargo.lock index e9391931..a31bb802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "PMOMusic" -version = "0.3.33" +version = "0.3.35" dependencies = [ "axum 0.8.7", "console-subscriber", diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index f5123ab0..aaf4f5e3 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "PMOMusic" -version = "0.3.33" +version = "0.3.35" edition = "2024" [dependencies] diff --git a/pmomediarenderer/src/handlers.rs b/pmomediarenderer/src/handlers.rs index 24b8a7e1..8ff4c795 100644 --- a/pmomediarenderer/src/handlers.rs +++ b/pmomediarenderer/src/handlers.rs @@ -18,9 +18,10 @@ pub fn play_handler( pipeline: PipelineHandle, state: SharedState, instance_id: String, + stream_url_base: String, ) -> ActionHandler { action_handler!( - captures(pipeline, state, instance_id) | data | { + captures(pipeline, state, instance_id, stream_url_base) | data | { tracing::info!("[MediaRenderer] UPnP Play action invoked"); let has_uri = state.read().current_uri.is_some(); if !has_uri { @@ -31,7 +32,7 @@ pub fn play_handler( let mut s = state.write(); s.playback_state = PlaybackState::Transitioning; s.push_command(crate::adapter::DeviceCommand::Stream { - url: format!("/api/webrenderer/{}/stream", instance_id), + url: format!("{}/{}/stream", stream_url_base, instance_id), }); } pipeline.flac_handle.resume(); diff --git a/pmomediarenderer/src/lib.rs b/pmomediarenderer/src/lib.rs index 80dfcad9..588fd0e2 100644 --- a/pmomediarenderer/src/lib.rs +++ b/pmomediarenderer/src/lib.rs @@ -26,7 +26,7 @@ pub mod state; pub use error::MediaRendererError; pub use handlers::*; -pub use messages::{PlaybackState, PlayerStateReport}; +pub use messages::PlaybackState; pub use pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds, InstancePipeline}; pub use registry::{MediaRendererInstance, MediaRendererRegistry}; pub use state::{RendererState, SharedState}; diff --git a/pmomediarenderer/src/messages.rs b/pmomediarenderer/src/messages.rs index cab243c3..e4042028 100644 --- a/pmomediarenderer/src/messages.rs +++ b/pmomediarenderer/src/messages.rs @@ -11,11 +11,3 @@ pub enum PlaybackState { Transitioning, } -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct PlayerStateReport { - pub position_sec: Option, - pub duration_sec: Option, - pub state: Option, - pub ready_state: Option, -} diff --git a/pmomediarenderer/src/registry.rs b/pmomediarenderer/src/registry.rs index 960c39d5..a452afff 100644 --- a/pmomediarenderer/src/registry.rs +++ b/pmomediarenderer/src/registry.rs @@ -63,8 +63,9 @@ impl MediaRendererRegistry { pub async fn register_or_reconnect( &self, instance_id: &str, - user_agent: &str, - adapter: Arc, + stream_url_base: &str, + renderer_name: &str, + adapter_fn: impl FnOnce(SharedState) -> Arc, ) -> Result<(String, String, bool), MediaRendererError> { if let Some(cancel) = self.pending_unregister.write().remove(instance_id) { tracing::info!(instance_id = %instance_id, "MediaRenderer: cancelled pending unregister (page reload)"); @@ -76,8 +77,8 @@ impl MediaRendererRegistry { if let Some(existing) = instances.get(instance_id) { tracing::info!(instance_id = %instance_id, "MediaRenderer: reconnecting existing instance"); #[cfg(feature = "pmoserver")] - self.register_with_control_point(&existing.device_instance)?; - let stream_url = format!("/api/webrenderer/{}/stream", instance_id); + self.register_with_control_point(&existing.device_instance, renderer_name)?; + let stream_url = format!("{}/{}/stream", stream_url_base, instance_id); let should_play = { let s = existing.state.read(); s.current_uri.is_some() && matches!( @@ -89,9 +90,9 @@ impl MediaRendererRegistry { } } - let instance = self.create_instance_with_adapter(instance_id, user_agent, adapter).await?; + let instance = self.create_instance_with_adapter(instance_id, stream_url_base, renderer_name, adapter_fn).await?; let instance = Arc::new(instance); - let stream_url = format!("/api/webrenderer/{}/stream", instance_id); + let stream_url = format!("{}/{}/stream", stream_url_base, instance_id); let udn = instance.udn.clone(); { @@ -223,47 +224,14 @@ impl MediaRendererRegistry { }); } - pub async fn update_player_state( - &self, - instance_id: &str, - report: crate::messages::PlayerStateReport, - ) { - let instances = self.instances.read(); - if let Some(instance) = instances.get(instance_id) { - let mut state = instance.state.write(); - if let Some(pos) = report.position_sec { - state.position = Some(crate::pipeline::seconds_to_upnp_time(pos)); - } - if let Some(dur) = report.duration_sec { - state.duration = Some(crate::pipeline::seconds_to_upnp_time(dur)); - } - if let Some(s) = &report.state { - state.playback_state = match s.as_str() { - "playing" => crate::messages::PlaybackState::Playing, - "paused" => crate::messages::PlaybackState::Paused, - "stopped" => crate::messages::PlaybackState::Stopped, - _ => state.playback_state.clone(), - }; - } - tracing::debug!(instance_id = %instance_id, position = ?state.position, "player state updated"); - } - } - - pub async fn get_pending_command( - &self, - instance_id: &str, - ) -> Option { - let state = self.instances.read().get(instance_id).map(|i| i.state.clone())?; - let cmd = state.write().pop_command()?; - serde_json::to_value(cmd).ok() - } - - /// Créer une nouvelle instance avec un adapter fourni (permet à l'appelant de créer BrowserAdapter) + /// Créer une nouvelle instance avec un adapter fourni via une factory closure. + /// La closure reçoit le SharedState de l'instance afin que l'adapter partage le même état. pub async fn create_instance_with_adapter( &self, instance_id: &str, - user_agent: &str, - adapter: Arc, + stream_url_base: &str, + renderer_name: &str, + adapter_fn: impl FnOnce(SharedState) -> Arc, ) -> Result { let candidate_udn = instance_id.to_ascii_lowercase(); let full_udn = format!("uuid:{}", candidate_udn); @@ -277,6 +245,7 @@ impl MediaRendererRegistry { } let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default())); + let adapter = adapter_fn(state.clone()); #[cfg(feature = "pmoserver")] let (device_instance, pipeline) = { @@ -306,9 +275,10 @@ impl MediaRendererRegistry { let device = MediaRendererFactory::create_device_with_pipeline( instance_id, "MediaRenderer", - user_agent, + "WebRenderer", pipeline.clone(), state.clone(), + stream_url_base, ) .map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?; @@ -319,7 +289,7 @@ impl MediaRendererRegistry { .map_err(|e| MediaRendererError::RegistrationError(e.to_string()))? }; - self.register_with_control_point(&di)?; + self.register_with_control_point(&di, renderer_name)?; (di, ip) }; @@ -337,9 +307,10 @@ impl MediaRendererRegistry { let device = MediaRendererFactory::create_device_with_pipeline( instance_id, "MediaRenderer", - user_agent, + "WebRenderer", pipeline.clone(), state.clone(), + stream_url_base, ) .map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?; @@ -362,6 +333,7 @@ impl MediaRendererRegistry { fn register_with_control_point( &self, di: &Arc, + renderer_name: &str, ) -> Result<(), MediaRendererError> { let base_url = di.base_url().to_string(); let udn = di.udn().to_ascii_lowercase(); @@ -397,7 +369,7 @@ impl MediaRendererRegistry { ..Default::default() }, format!("{}{}", base_url, di.description_route()), - "PMOMusic WebRenderer/2.0".to_string(), + renderer_name.to_string(), Some("urn:schemas-upnp-org:service:AVTransport:1".to_string()), avtransport_control_url, Some("urn:schemas-upnp-org:service:RenderingControl:1".to_string()), diff --git a/pmomediarenderer/src/renderer.rs b/pmomediarenderer/src/renderer.rs index 5f4b854a..d4be0cba 100644 --- a/pmomediarenderer/src/renderer.rs +++ b/pmomediarenderer/src/renderer.rs @@ -87,20 +87,20 @@ impl MediaRendererFactory { pub fn create_device_with_pipeline( device_name: &str, device_type: &str, - device_ua: &str, + friendly_name_suffix: &str, pipeline: PipelineHandle, state: SharedState, + stream_url_base: &str, ) -> Result { - let avtransport = Self::build_avtransport(pipeline.clone(), state.clone(), device_name)?; + let avtransport = Self::build_avtransport(pipeline.clone(), state.clone(), device_name, stream_url_base)?; let renderingcontrol = Self::build_renderingcontrol(state.clone())?; let connectionmanager = Self::build_connectionmanager()?; - let mut device = Device::new( + let device = Device::new_from_config( device_name.to_string(), device_type.to_string(), - device_ua.to_string(), + friendly_name_suffix.to_string(), ); - device.set_model_name("MediaRenderer".to_string()); device .add_service(Arc::new(avtransport)) .map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?; @@ -118,6 +118,7 @@ impl MediaRendererFactory { pipeline: PipelineHandle, state: SharedState, instance_id: &str, + stream_url_base: &str, ) -> Result { let mut svc = Service::new("AVTransport".to_string()); @@ -151,6 +152,7 @@ impl MediaRendererFactory { pipeline.clone(), state.clone(), instance_id.to_string(), + stream_url_base.to_string(), )); add_action(&mut svc, Arc::new(play))?; diff --git a/pmowebrenderer/src/register.rs b/pmowebrenderer/src/register.rs index cd76078b..834e60a1 100644 --- a/pmowebrenderer/src/register.rs +++ b/pmowebrenderer/src/register.rs @@ -14,10 +14,19 @@ use std::sync::Arc; use pmomediarenderer::PlaybackState; use pmomediarenderer::PipelineControl; -use pmomediarenderer::{DeviceAdapter, DeviceCommand, MediaRendererRegistry, PlayerStateReport, SharedState}; +use pmomediarenderer::{DeviceCommand, MediaRendererRegistry}; use crate::adapter::BrowserAdapter; +#[derive(Debug, serde::Deserialize)] +pub struct PlayerStateReport { + pub position_sec: Option, + pub duration_sec: Option, + pub state: Option, + #[allow(dead_code)] + pub ready_state: Option, +} + #[derive(Debug, Deserialize)] pub struct RegisterRequest { pub instance_id: String, @@ -42,13 +51,13 @@ pub async fn register_handler( "WebRenderer: register request" ); - let state: SharedState = Arc::new(parking_lot::RwLock::new( - pmomediarenderer::RendererState::default() - )); - let adapter: Arc = Arc::new(BrowserAdapter::new(state)); - match registry - .register_or_reconnect(&req.instance_id, &req.user_agent, adapter) + .register_or_reconnect( + &req.instance_id, + "/api/webrenderer", + "PMOMusic WebRenderer/2.0", + |state| Arc::new(BrowserAdapter::new(state)), + ) .await { Ok((stream_url, udn, should_play)) => { @@ -135,8 +144,27 @@ pub async fn report_handler( Path(instance_id): Path, Json(report): Json, ) -> impl IntoResponse { - registry.update_player_state(&instance_id, report).await; - StatusCode::OK + let instance = match registry.get_instance(&instance_id) { + Some(i) => i, + None => return StatusCode::NOT_FOUND.into_response(), + }; + let mut state = instance.state.write(); + if let Some(pos) = report.position_sec { + state.position = Some(pmomediarenderer::seconds_to_upnp_time(pos)); + } + if let Some(dur) = report.duration_sec { + state.duration = Some(pmomediarenderer::seconds_to_upnp_time(dur)); + } + if let Some(s) = &report.state { + state.playback_state = match s.as_str() { + "playing" => PlaybackState::Playing, + "paused" => PlaybackState::Paused, + "stopped" => PlaybackState::Stopped, + _ => state.playback_state.clone(), + }; + } + tracing::debug!(instance_id = %instance_id, position = ?state.position, "player state updated"); + StatusCode::OK.into_response() } #[axum::debug_handler] @@ -144,8 +172,12 @@ pub async fn command_handler( State(registry): State>, Path(instance_id): Path, ) -> impl IntoResponse { - match registry.get_pending_command(&instance_id).await { - Some(cmd) => (StatusCode::OK, Json(cmd)).into_response(), + let Some(instance) = registry.get_instance(&instance_id) else { + return StatusCode::NO_CONTENT.into_response(); + }; + let cmd = instance.state.write().pop_command(); + match cmd.and_then(|c| serde_json::to_value(c).ok()) { + Some(v) => (StatusCode::OK, Json(v)).into_response(), None => StatusCode::NO_CONTENT.into_response(), } } diff --git a/version.txt b/version.txt index 55cebfb9..1ff34fb3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.3.33 +0.3.35