⬆️ version bump to v0.3.6
- Bump crate and app versions from `v0.3.5` to v"\n- Add retry logic with exponential backoff in `fetchRenderers`[39m - Improve re-registration robustness in `useWebRenderer` (stale player handling, instanceId error recovery) - Trigger forced renderer refresh after successful registration
This commit is contained in:
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const sharedRendererUdn = ref<string | null>(null);
|
||||
const sharedRendererUdn = ref<string | null>(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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<dyn DeviceAdapter>,
|
||||
) -> 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<dyn DeviceAdapter>,
|
||||
) -> Result<MediaRendererInstance, MediaRendererError> {
|
||||
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<DeviceInstance>,
|
||||
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();
|
||||
|
||||
@@ -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<Device, FactoryError> {
|
||||
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))
|
||||
|
||||
15
pmowebrenderer/src/helpers.rs
Normal file
15
pmowebrenderer/src/helpers.rs
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user