diff --git a/pmoapp/webapp/src/components/pmocontrol/VolumeControl.vue b/pmoapp/webapp/src/components/pmocontrol/VolumeControl.vue index 05b3f867..d7787592 100644 --- a/pmoapp/webapp/src/components/pmocontrol/VolumeControl.vue +++ b/pmoapp/webapp/src/components/pmocontrol/VolumeControl.vue @@ -1,286 +1,292 @@ diff --git a/pmoapp/webapp/src/components/unified/BottomTabBar.vue b/pmoapp/webapp/src/components/unified/BottomTabBar.vue index 3dfec76a..4d617c0f 100644 --- a/pmoapp/webapp/src/components/unified/BottomTabBar.vue +++ b/pmoapp/webapp/src/components/unified/BottomTabBar.vue @@ -1,452 +1,377 @@ diff --git a/pmoapp/webapp/src/components/unified/RendererDrawer.vue b/pmoapp/webapp/src/components/unified/RendererDrawer.vue new file mode 100644 index 00000000..763c93e5 --- /dev/null +++ b/pmoapp/webapp/src/components/unified/RendererDrawer.vue @@ -0,0 +1,762 @@ + + + + + diff --git a/pmoapp/webapp/src/components/unified/RendererTabContent.vue b/pmoapp/webapp/src/components/unified/RendererTabContent.vue index 804eac38..12950107 100644 --- a/pmoapp/webapp/src/components/unified/RendererTabContent.vue +++ b/pmoapp/webapp/src/components/unified/RendererTabContent.vue @@ -6,9 +6,7 @@ import CurrentTrack from "@/components/pmocontrol/CurrentTrack.vue"; import TransportControls from "@/components/pmocontrol/TransportControls.vue"; import VolumeControl from "@/components/pmocontrol/VolumeControl.vue"; import QueueViewer from "@/components/pmocontrol/QueueViewer.vue"; -import StatusBadge from "@/components/pmocontrol/StatusBadge.vue"; -import { ChevronUp, ChevronDown, Link } from "lucide-vue-next"; -import { useRenderers } from "@/composables/useRenderers"; +import { ChevronUp, ChevronDown } from "lucide-vue-next"; import { useUIStore } from "@/stores/ui"; import { api } from "@/services/pmocontrol/api"; import type { QueueItem } from "@/services/pmocontrol/types"; @@ -17,10 +15,9 @@ const props = defineProps<{ rendererId: string; }>(); -const { renderer, state, queue, binding, refresh } = useRenderer( +const { renderer, state, queue, refresh } = useRenderer( toRef(props, "rendererId"), ); -const { detachPlaylist } = useRenderers(); const uiStore = useUIStore(); // Détection mobile portrait pour afficher le drawer au lieu de la colonne @@ -42,22 +39,6 @@ onMounted(async () => { // État du renderer pour affichage const isOnline = computed(() => renderer.value?.online ?? false); -const transportState = computed( - () => state.value?.transport_state ?? "STOPPED", -); -const hasPlaylistBinding = computed(() => !!binding.value); - -// Détacher la playlist -async function handleDetachPlaylist() { - try { - await detachPlaylist(props.rendererId); - uiStore.notifySuccess("Playlist détachée"); - } catch (error) { - uiStore.notifyError( - `Erreur: ${error instanceof Error ? error.message : "Erreur inconnue"}`, - ); - } -} // Gérer le clic sur un item de la queue async function handleQueueItemClick(item: QueueItem) { @@ -86,42 +67,7 @@ async function handleQueueItemClick(item: QueueItem) { diff --git a/pmocontrol/src/discovery/upnp_discovery.rs b/pmocontrol/src/discovery/upnp_discovery.rs index b39c52cf..39571819 100644 --- a/pmocontrol/src/discovery/upnp_discovery.rs +++ b/pmocontrol/src/discovery/upnp_discovery.rs @@ -1,14 +1,25 @@ use crate::{DeviceRegistry, discovery::upnp_provider::ParsedDeviceDescription}; +use crossbeam_channel::{Sender, bounded}; use pmoupnp::ssdp::SsdpEvent; use std::sync::{Arc, Mutex, RwLock}; +use std::thread; use crate::discovery::manager::UDNRegistry; -/// Gestionnaire des événements SSDP -> DeviceUpdate. +/// Task to fetch a device description +struct FetchTask { + udn: String, + location: String, + server_header: String, + max_age: u32, + registry: Arc>, +} +/// Gestionnaire des événements SSDP -> DeviceUpdate. pub struct UpnpDiscoveryManager { device_registry: Arc>, udn_cache: Arc>, + fetch_sender: Sender, } impl UpnpDiscoveryManager { @@ -16,9 +27,39 @@ impl UpnpDiscoveryManager { device_registry: Arc>, udn_cache: Arc>, ) -> Self { + // Create a bounded channel for fetch tasks (max 10 pending tasks) + let (sender, receiver) = bounded::(10); + + // Spawn a pool of 3 worker threads to process fetch tasks + for _ in 0..3 { + let receiver = receiver.clone(); + thread::spawn(move || { + while let Ok(task) = receiver.recv() { + // Fetch + parse the device description (may take up to 5 seconds) + if let Ok(info) = ParsedDeviceDescription::new( + &task.udn, + &task.location, + &task.server_header, + 5, + ) { + if let Some(renderer_info) = info.build_renderer() { + if let Ok(mut reg) = task.registry.write() { + reg.push_renderer(&renderer_info, task.max_age); + } + } else if let Some(server_info) = info.build_server() { + if let Ok(mut reg) = task.registry.write() { + reg.push_server(&server_info, task.max_age); + } + } + } + } + }); + } + Self { device_registry, udn_cache, + fetch_sender: sender, } } @@ -48,20 +89,18 @@ impl UpnpDiscoveryManager { UDNRegistry::should_fetch(self.udn_cache.clone(), &udn, max_age as u64); if should_fetch { - // Fetch + parse the device description - if let Ok(info) = - ParsedDeviceDescription::new(&udn, &location, &server_header, 5) - { - if let Some(renderer_info) = info.build_renderer() { - if let Ok(mut reg) = self.device_registry.write() { - reg.push_renderer(&renderer_info, max_age); - } - } else if let Some(server_info) = info.build_server() { - if let Ok(mut reg) = self.device_registry.write() { - reg.push_server(&server_info, max_age); - } - } - } + // Send fetch task to worker pool (non-blocking) + // If the channel is full, try_send will fail and we skip this fetch + let task = FetchTask { + udn: udn.clone(), + location: location.clone(), + server_header: server_header.clone(), + max_age, + registry: Arc::clone(&self.device_registry), + }; + + // Use try_send to avoid blocking if the queue is full + let _ = self.fetch_sender.try_send(task); } else { // Even if we don't fetch, we MUST update last_seen to prevent timeout // This is critical: SSDP Alive messages arrive more frequently than max_age/2, diff --git a/pmocontrol/src/pmoserver_ext.rs b/pmocontrol/src/pmoserver_ext.rs index 8329cc52..8bbb8906 100644 --- a/pmocontrol/src/pmoserver_ext.rs +++ b/pmocontrol/src/pmoserver_ext.rs @@ -91,22 +91,29 @@ impl ControlPointState { tag = "control" )] async fn list_renderers(State(state): State) -> Json> { - let renderers = state.control_point.list_music_renderers(); + // Use spawn_blocking to avoid blocking the tokio runtime + // This is critical because list_music_renderers acquires a RwLock + let control_point = state.control_point.clone(); + let summaries = tokio::task::spawn_blocking(move || { + let renderers = control_point.list_music_renderers(); - let summaries: Vec = renderers - .into_iter() - .map(|r| { - let info = r.info(); - RendererSummary { - id: r.id().0.clone(), - friendly_name: r.friendly_name().to_string(), - model_name: r.model_name().to_string(), - protocol: protocol_summary(&info.protocol()), - capabilities: capability_summary(&info.capabilities()), - online: r.is_online(), - } - }) - .collect(); + renderers + .into_iter() + .map(|r| { + let info = r.info(); + RendererSummary { + id: r.id().0.clone(), + friendly_name: r.friendly_name().to_string(), + model_name: r.model_name().to_string(), + protocol: protocol_summary(&info.protocol()), + capabilities: capability_summary(&info.capabilities()), + online: r.is_online(), + } + }) + .collect::>() + }) + .await + .unwrap_or_default(); Json(summaries) } @@ -156,10 +163,22 @@ async fn get_renderer_full_snapshot( Path(renderer_id): Path, ) -> Result, (StatusCode, Json)> { let rid = DeviceId(renderer_id.clone()); - let snapshot = state - .control_point - .renderer_full_snapshot(&rid) - .map_err(|err| map_snapshot_error(renderer_id, err))?; + + // Use spawn_blocking because renderer_full_snapshot does sync UPnP calls + let control_point = state.control_point.clone(); + let rid_clone = rid.clone(); + let snapshot = + tokio::task::spawn_blocking(move || control_point.renderer_full_snapshot(&rid_clone)) + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: format!("Task error: {}", e), + }), + ) + })? + .map_err(|err| map_snapshot_error(renderer_id, err))?; Ok(Json(snapshot)) } @@ -1519,17 +1538,23 @@ async fn add_after_current( tag = "control" )] async fn list_servers(State(state): State) -> Json> { - let servers = state.control_point.list_media_servers().unwrap_or_default(); + // Use spawn_blocking to avoid blocking the tokio runtime + let control_point = state.control_point.clone(); + let summaries = tokio::task::spawn_blocking(move || { + let servers = control_point.list_media_servers().unwrap_or_default(); - let summaries: Vec = servers - .into_iter() - .map(|s| MediaServerSummary { - id: s.id().0.clone(), - friendly_name: s.friendly_name().to_string(), - model_name: s.model_name().to_string(), - online: s.is_online(), - }) - .collect(); + servers + .into_iter() + .map(|s| MediaServerSummary { + id: s.id().0.clone(), + friendly_name: s.friendly_name().to_string(), + model_name: s.model_name().to_string(), + online: s.is_online(), + }) + .collect::>() + }) + .await + .unwrap_or_default(); Json(summaries) } diff --git a/pmoupnp/src/ssdp/server.rs b/pmoupnp/src/ssdp/server.rs index e07d9d0f..4ec816fe 100644 --- a/pmoupnp/src/ssdp/server.rs +++ b/pmoupnp/src/ssdp/server.rs @@ -83,6 +83,7 @@ impl SsdpServer { &"0.0.0.0".parse().unwrap(), )?; + socket.set_read_timeout(Some(Duration::from_secs(1)))?; socket.set_multicast_loop_v4(false)?; let socket = Arc::new(socket); @@ -274,6 +275,10 @@ impl SsdpServer { } } } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Timeout, continuer + continue; + } Err(e) => { warn!("❌ SSDP read error: {}", e); }