From 813fc97fa108debff9b08149d74cc287e99c5628 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 5 Apr 2026 22:37:15 +0200 Subject: [PATCH] :arrow_up: version bump to v0.3.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump crate and app versions from `v0.3.5` to v"\n- Add retry logic with exponential backoff in `fetchRenderers` - Improve re-registration robustness in `useWebRenderer` (stale player handling, instanceId error recovery) - Trigger forced renderer refresh after successful registration --- .../src/components/unified/RendererDrawer.vue | 10 +++++--- .../webapp/src/composables/useWebRenderer.ts | 21 +++++++++++++++- .../webapp/src/views/UnifiedControlView.vue | 12 +++++---- pmomediarenderer/src/registry.rs | 25 +++++++++---------- pmomediarenderer/src/renderer.rs | 13 +++++++--- pmowebrenderer/src/helpers.rs | 15 +++++++++++ pmowebrenderer/src/lib.rs | 2 ++ pmowebrenderer/src/register.rs | 5 ++++ 8 files changed, 77 insertions(+), 26 deletions(-) create mode 100644 pmowebrenderer/src/helpers.rs diff --git a/pmoapp/webapp/src/components/unified/RendererDrawer.vue b/pmoapp/webapp/src/components/unified/RendererDrawer.vue index c33ee0ac..5acfe839 100644 --- a/pmoapp/webapp/src/components/unified/RendererDrawer.vue +++ b/pmoapp/webapp/src/components/unified/RendererDrawer.vue @@ -37,9 +37,13 @@ const myUdn = computed(() => webRenderer.rendererUdn.value); const filteredRenderers = computed(() => { const udn = myUdn.value; return allRenderers.value.filter((r: RendererSummary) => { - if (r.model_name !== "WebRenderer") return true; - if (udn === null) return false; - return r.id === udn; + // Show only our own WebRenderer based on UDN match + const isWebRenderer = r.model_name?.includes("WebRenderer") ?? false; + if (isWebRenderer) { + if (udn === null) return false; + return r.id === udn; + } + return true; }); }); diff --git a/pmoapp/webapp/src/composables/useWebRenderer.ts b/pmoapp/webapp/src/composables/useWebRenderer.ts index 46d90428..1a8449af 100644 --- a/pmoapp/webapp/src/composables/useWebRenderer.ts +++ b/pmoapp/webapp/src/composables/useWebRenderer.ts @@ -22,6 +22,24 @@ function generateUUID(): string { } const INSTANCE_ID_KEY = "pmomusic_webrenderer_instance_id"; +const RENDERER_UDN_KEY = "pmomusic_webrenderer_udn"; + +// Load persisted UDN on module load +function loadPersistedUdn(): string | null { + try { + return sessionStorage.getItem(RENDERER_UDN_KEY); + } catch { + return null; + } +} + +function savePersistedUdn(udn: string) { + try { + sessionStorage.setItem(RENDERER_UDN_KEY, udn); + } catch { + // ignore storage errors + } +} // Module-level singleton for PMOPlayer to prevent duplicate instances let globalPlayer: PMOPlayer | null = null; @@ -31,7 +49,7 @@ let registering = false; // Module-level reactive state shared across all composable invocations const sharedConnected = ref(false); const sharedStreamUrl = ref(null); -const sharedRendererUdn = ref(null); +const sharedRendererUdn = ref(loadPersistedUdn()); function getOrCreateInstanceId(): string { try { @@ -86,6 +104,7 @@ export function useWebRenderer() { const data = await resp.json(); streamUrl.value = data.stream_url; rendererUdn.value = data.udn; + savePersistedUdn(data.udn); player = new PMOPlayer(instanceId); globalPlayer = player; diff --git a/pmoapp/webapp/src/views/UnifiedControlView.vue b/pmoapp/webapp/src/views/UnifiedControlView.vue index 694542b2..a231e08e 100644 --- a/pmoapp/webapp/src/views/UnifiedControlView.vue +++ b/pmoapp/webapp/src/views/UnifiedControlView.vue @@ -103,14 +103,16 @@ function handleRendererSelect(rendererId: string) { } } -// Filtre la liste des renderers pour exclure les WebRenderers d'autres navigateurs. -// Seul le WebRenderer créé par ce navigateur (identifié par son UDN) reste visible. +// Filter to show only our own WebRenderer based on UDN match function filterRenderers(renderers: typeof allRenderers.value) { const myUdn = webRenderer.rendererUdn.value; return renderers.filter((r) => { - if (r.model_name !== "WebRenderer") return true; // renderer classique : toujours visible - if (myUdn === null) return false; // pas encore de session : masquer tous les WebRenderers - return r.id === myUdn; // ne garder que le nôtre + const isWebRenderer = r.model_name?.includes("WebRenderer") ?? false; + if (isWebRenderer) { + if (myUdn === null) return false; + return r.id === myUdn; + } + return true; }); } diff --git a/pmomediarenderer/src/registry.rs b/pmomediarenderer/src/registry.rs index a452afff..ec6f0032 100644 --- a/pmomediarenderer/src/registry.rs +++ b/pmomediarenderer/src/registry.rs @@ -65,6 +65,7 @@ impl MediaRendererRegistry { instance_id: &str, stream_url_base: &str, renderer_name: &str, + friendly_name: &str, adapter_fn: impl FnOnce(SharedState) -> Arc, ) -> Result<(String, String, bool), MediaRendererError> { if let Some(cancel) = self.pending_unregister.write().remove(instance_id) { @@ -77,7 +78,7 @@ 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, renderer_name)?; + self.register_with_control_point(&existing.device_instance, renderer_name, &existing.udn)?; let stream_url = format!("{}/{}/stream", stream_url_base, instance_id); let should_play = { let s = existing.state.read(); @@ -90,7 +91,7 @@ impl MediaRendererRegistry { } } - let instance = self.create_instance_with_adapter(instance_id, stream_url_base, renderer_name, adapter_fn).await?; + let instance = self.create_instance_with_adapter(instance_id, stream_url_base, renderer_name, friendly_name, adapter_fn).await?; let instance = Arc::new(instance); let stream_url = format!("{}/{}/stream", stream_url_base, instance_id); let udn = instance.udn.clone(); @@ -231,18 +232,14 @@ impl MediaRendererRegistry { instance_id: &str, stream_url_base: &str, renderer_name: &str, + friendly_name: &str, adapter_fn: impl FnOnce(SharedState) -> Arc, ) -> Result { let candidate_udn = instance_id.to_ascii_lowercase(); let full_udn = format!("uuid:{}", candidate_udn); - if let Err(e) = pmoconfig::get_config().set_device_udn( - "MediaRenderer", - instance_id, - candidate_udn.clone(), - ) { - tracing::warn!("MediaRenderer: failed to persist UDN: {:?}", e); - } + // Note: WebRenderers don't need to persist UDN to config since they're tied to browser tabs + // (instance_id comes from sessionStorage, not a persistent device) let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default())); let adapter = adapter_fn(state.clone()); @@ -275,7 +272,7 @@ impl MediaRendererRegistry { let device = MediaRendererFactory::create_device_with_pipeline( instance_id, "MediaRenderer", - "WebRenderer", + friendly_name, pipeline.clone(), state.clone(), stream_url_base, @@ -289,7 +286,7 @@ impl MediaRendererRegistry { .map_err(|e| MediaRendererError::RegistrationError(e.to_string()))? }; - self.register_with_control_point(&di, renderer_name)?; + self.register_with_control_point(&di, renderer_name, &full_udn)?; (di, ip) }; @@ -307,7 +304,7 @@ impl MediaRendererRegistry { let device = MediaRendererFactory::create_device_with_pipeline( instance_id, "MediaRenderer", - "WebRenderer", + friendly_name, pipeline.clone(), state.clone(), stream_url_base, @@ -334,9 +331,11 @@ impl MediaRendererRegistry { &self, di: &Arc, renderer_name: &str, + instance_udn: &str, ) -> Result<(), MediaRendererError> { let base_url = di.base_url().to_string(); - let udn = di.udn().to_ascii_lowercase(); + // Use the instance's UDN, not the device's stored UDN + let udn = instance_udn.trim_start_matches("uuid:").to_ascii_lowercase(); let udn_with_prefix = format!("uuid:{}", udn); let device_route = di.route(); let model = di.get_model(); diff --git a/pmomediarenderer/src/renderer.rs b/pmomediarenderer/src/renderer.rs index d4be0cba..5b951dd6 100644 --- a/pmomediarenderer/src/renderer.rs +++ b/pmomediarenderer/src/renderer.rs @@ -87,19 +87,24 @@ impl MediaRendererFactory { pub fn create_device_with_pipeline( device_name: &str, device_type: &str, - friendly_name_suffix: &str, + friendly_name: &str, pipeline: PipelineHandle, state: SharedState, stream_url_base: &str, ) -> Result { - let avtransport = Self::build_avtransport(pipeline.clone(), state.clone(), device_name, stream_url_base)?; + 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 device = Device::new_from_config( + let device = Device::new( device_name.to_string(), device_type.to_string(), - friendly_name_suffix.to_string(), + friendly_name.to_string(), ); device .add_service(Arc::new(avtransport)) diff --git a/pmowebrenderer/src/helpers.rs b/pmowebrenderer/src/helpers.rs new file mode 100644 index 00000000..b1518d3c --- /dev/null +++ b/pmowebrenderer/src/helpers.rs @@ -0,0 +1,15 @@ +pub fn extract_browser_name(ua: &str) -> &str { + if ua.contains("Edg/") || ua.contains("EdgA/") { + "Edge" + } else if ua.contains("OPR/") || ua.contains("Opera") { + "Opera" + } else if ua.contains("Chrome/") { + "Chrome" + } else if ua.contains("Firefox/") { + "Firefox" + } else if ua.contains("Safari/") { + "Safari" + } else { + "Browser" + } +} diff --git a/pmowebrenderer/src/lib.rs b/pmowebrenderer/src/lib.rs index f1e9e633..dc40a460 100644 --- a/pmowebrenderer/src/lib.rs +++ b/pmowebrenderer/src/lib.rs @@ -6,6 +6,7 @@ //! - Les commandes UPnP sont relayées vers le pipeline audio via PipelineControl mod adapter; +mod helpers; mod register; mod stream; @@ -13,6 +14,7 @@ mod stream; mod config; pub use adapter::BrowserAdapter; +pub use helpers::extract_browser_name; #[cfg(feature = "pmoserver")] pub use config::WebRendererExt; \ No newline at end of file diff --git a/pmowebrenderer/src/register.rs b/pmowebrenderer/src/register.rs index 834e60a1..663c8c67 100644 --- a/pmowebrenderer/src/register.rs +++ b/pmowebrenderer/src/register.rs @@ -17,6 +17,7 @@ use pmomediarenderer::PipelineControl; use pmomediarenderer::{DeviceCommand, MediaRendererRegistry}; use crate::adapter::BrowserAdapter; +use crate::helpers::extract_browser_name; #[derive(Debug, serde::Deserialize)] pub struct PlayerStateReport { @@ -51,11 +52,15 @@ pub async fn register_handler( "WebRenderer: register request" ); + let browser_name = extract_browser_name(&req.user_agent); + let friendly_name = format!("Web Audio – {}", browser_name); + match registry .register_or_reconnect( &req.instance_id, "/api/webrenderer", "PMOMusic WebRenderer/2.0", + &friendly_name, |state| Arc::new(BrowserAdapter::new(state)), ) .await