⬆️ migration mdns →mddns-sd

- Remplacer la crate abandonnée `mdns` (3.0, non-UTF8) par activement maintenue et UTF‑compliant `mdns-sd` (0.19)
- Supprimer les dépendances inutiles `async‑std`, `futures-util` (uniquement utilisées dans le thread mDNS)
- Refactoriser `control_point.rs` : passer d’un bloc async bloquant à une boucle synchrone avec `ServiceDaemon.browse()` + récepteur blocant
- Simplifier drastiquement le parsing dans `chromecast_discovery.rs` via l’API haut‑niveau de mdns-sd (`ServiceInfo`, `get_property_val_str`) → ~80 lignes remplacées par 52
- Ajouter index SQLite `idx_metadata_key_value` pour accélérer les requêtes par origin_url
- Nettoyer exports inutiles dans `pmomediaserver::server_ext`
- Supprimer filtre de bruit obsolète `mdns=error` dans les logs
- Met à jour la version en 0.3.42
This commit is contained in:
2026-04-09 15:49:07 +02:00
parent 35e2e037f0
commit 934bbaf4b8
12 changed files with 580 additions and 495 deletions

View File

@@ -20,9 +20,7 @@ ratatui = { version = "0.26", default-features = false, features = ["crossterm"]
crossterm = "0.27"
rust_cast = "0.19"
rustls = { version = "0.23", features = ["aws-lc-rs"] }
mdns = "3.0"
async-std = "1.12"
futures-util = "0.3"
mdns-sd = "0.19"
smol = "2.0"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@@ -13,7 +13,7 @@ use tracing::{debug, error, info, warn};
use crate::discovery::manager::UDNRegistry;
use crate::errors::ControlPointError;
use crate::events::{MediaServerEventBus, RendererEventBus};
use crate::media_server::{MediaBrowser, MusicServer, playback_item_from_entry};
use crate::media_server::{playback_item_from_entry, MediaBrowser, MusicServer};
use crate::media_server_events::spawn_media_server_event_runtime;
use crate::model::{MediaServerEvent, RendererEvent};
use crate::model::{PlaybackState, TrackMetadata};
@@ -147,51 +147,52 @@ impl ControlPoint {
let udn_cache_for_mdns = Arc::clone(&udn_cache);
thread::spawn(move || {
use crate::discovery::ChromecastDiscoveryManager;
use futures_util::StreamExt;
use mdns_sd::{ServiceDaemon, ServiceEvent};
// Créer le gestionnaire de découverte UPNP
let mut discovery_manager =
ChromecastDiscoveryManager::new(registry_for_mdns, udn_cache_for_mdns);
debug!("Starting mDNS discovery thread for Chromecast devices");
const SERVICE_NAME: &str = "_googlecast._tcp.local";
const SERVICE_TYPE: &str = "_googlecast._tcp.local.";
// Run async discovery in a blocking task
async_std::task::block_on(async {
// Create mDNS discovery stream with 15 second query interval
// (shorter interval for faster initial discovery)
match mdns::discover::all(SERVICE_NAME, Duration::from_secs(15)) {
Ok(discovery) => {
let stream = discovery.listen();
futures_util::pin_mut!(stream);
debug!("mDNS discovery stream started for Chromecast devices");
// Listen to mDNS responses
while let Some(result) = stream.next().await {
match result {
Ok(response) => {
debug!(
"Received mDNS response with {} records",
response.records().count()
);
discovery_manager.handle_mdns_response(response);
}
Err(e) => {
warn!("mDNS discovery error: {}", e);
}
}
}
warn!("mDNS discovery stream ended unexpectedly");
}
Err(e) => {
error!("Failed to start mDNS discovery: {}", e);
}
let daemon = match ServiceDaemon::new() {
Ok(d) => d,
Err(e) => {
error!("Failed to create mDNS daemon: {}", e);
return;
}
});
};
let receiver = match daemon.browse(SERVICE_TYPE) {
Ok(r) => r,
Err(e) => {
error!("Failed to start mDNS browse for {}: {}", SERVICE_TYPE, e);
return;
}
};
debug!("mDNS discovery started for Chromecast devices");
while let Ok(event) = receiver.recv() {
match event {
ServiceEvent::ServiceResolved(info) => {
debug!(
fullname = info.get_fullname(),
host = info.get_hostname(),
port = info.get_port(),
"mDNS Chromecast service resolved"
);
discovery_manager.handle_service_resolved(&info);
}
ServiceEvent::ServiceRemoved(_service_type, fullname) => {
debug!(fullname = %fullname, "mDNS Chromecast service removed");
}
_ => {}
}
}
warn!("mDNS discovery receiver closed");
});
// Note: Renderer polling is now handled by each MusicRenderer's own watcher thread.
@@ -1169,24 +1170,21 @@ impl ControlPoint {
binding.auto_play_on_refresh = auto_play;
renderer.set_playlist_binding(Some(binding));
let callback: Option<Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>> =
if auto_play {
let reg = Arc::clone(&self.registry);
Some(Box::new(move |rid: &DeviceId| {
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(||
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
)?;
renderer.play_current_from_queue()
}))
} else {
None
};
let _ = schedule_queue_refresh_for(
&self.registry,
renderer_id,
&self.event_bus,
callback,
);
let callback: Option<
Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>,
> = if auto_play {
let reg = Arc::clone(&self.registry);
Some(Box::new(move |rid: &DeviceId| {
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(|| {
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
})?;
renderer.play_current_from_queue()
}))
} else {
None
};
let _ =
schedule_queue_refresh_for(&self.registry, renderer_id, &self.event_bus, callback);
return Ok(());
}
@@ -1240,22 +1238,23 @@ impl ControlPoint {
// Note: BindingChanged event is emitted automatically by MusicRenderer::set_playlist_binding()
// For initial attach with auto_play, force playback start (don't check if idle)
let callback: Option<Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>> =
if auto_play {
let reg = Arc::clone(&self.registry);
Some(Box::new(move |rid: &DeviceId| {
debug!(
renderer = rid.0.as_str(),
"Attach callback: forcing playback start (not checking if idle)"
);
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(||
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
)?;
renderer.play_current_from_queue()
}))
} else {
None
};
let callback: Option<
Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>,
> = if auto_play {
let reg = Arc::clone(&self.registry);
Some(Box::new(move |rid: &DeviceId| {
debug!(
renderer = rid.0.as_str(),
"Attach callback: forcing playback start (not checking if idle)"
);
let renderer = reg.read().unwrap().get_renderer(rid).ok_or_else(|| {
ControlPointError::ControlPoint(format!("Renderer {} not found", rid.0))
})?;
renderer.play_current_from_queue()
}))
} else {
None
};
let _ = schedule_queue_refresh_for(&self.registry, renderer_id, &self.event_bus, callback);
Ok(())
@@ -1806,7 +1805,9 @@ fn schedule_queue_refresh_for(
registry: &Arc<RwLock<DeviceRegistry>>,
renderer_id: &DeviceId,
event_bus: &RendererEventBus,
after_refresh: Option<Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>>,
after_refresh: Option<
Box<dyn FnOnce(&DeviceId) -> Result<(), ControlPointError> + Send + 'static>,
>,
) -> SyncScheduleOutcome {
// Step 1: Get renderer from registry
let renderer = {
@@ -1895,7 +1896,9 @@ fn schedule_queue_refresh_for(
};
match music_server {
Some(s) => fetch_queue_items_for(&s, &container_id_clone),
None => Err(ControlPointError::MediaServerError("Server not found".to_string())),
None => Err(ControlPointError::MediaServerError(
"Server not found".to_string(),
)),
}
});

View File

@@ -5,14 +5,15 @@
//! This module handles the discovery of Chromecast devices and registers them
//! directly into the `DeviceRegistry`.
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, Mutex, RwLock};
use crate::DeviceId;
use crate::DeviceRegistry;
use mdns_sd::ResolvedService;
use mdns_sd::ServiceInfo;
use crate::discovery::manager::UDNRegistry;
use crate::model::{RendererCapabilities, RendererInfo, RendererProtocol};
use crate::DeviceId;
use crate::DeviceRegistry;
use tracing::{debug, warn};
/// Gestionnaire des événements mDNS pour Chromecast.
@@ -32,146 +33,80 @@ impl ChromecastDiscoveryManager {
}
}
/// Traite une réponse mDNS pour un appareil Chromecast.
/// Traite un service Chromecast résolu par mDNS-SD.
///
/// Cette fonction parse les réponses de service discovery mDNS pour les appareils
/// Chromecast et les enregistre directement dans le registre.
pub fn handle_mdns_response(&mut self, response: mdns::Response) {
// Extract basic information from the mDNS response
let service_name = match response.records().find_map(|r| {
if let mdns::RecordKind::PTR(ref name) = r.kind {
Some(name.clone())
} else {
None
}
}) {
Some(name) => name,
None => {
warn!("No PTR record found in mDNS response");
return;
}
};
/// `ServiceInfo` arrive pré-assemblé : plus besoin de jointure manuelle
/// des enregistrements PTR / A / SRV / TXT.
pub fn handle_service_resolved(&mut self, info: &ResolvedService) {
let fullname = info.get_fullname().to_string();
debug!("Processing mDNS response for service: {}", service_name);
debug!("Processing resolved Chromecast service: {}", fullname);
// Extract IP addresses
let addresses: Vec<IpAddr> = response
.records()
.filter_map(|r| match r.kind {
mdns::RecordKind::A(addr) => Some(IpAddr::V4(addr)),
mdns::RecordKind::AAAA(addr) => Some(IpAddr::V6(addr)),
_ => None,
})
.collect();
if addresses.is_empty() {
warn!(
"No IP address found for Chromecast device: {}",
service_name
);
return;
}
// Prefer IPv4 addresses
let host = match addresses
let host = match info
.get_addresses()
.iter()
.find(|addr| matches!(addr, IpAddr::V4(_)))
.or_else(|| addresses.first())
.find(|a| a.is_ipv4())
.or_else(|| info.get_addresses().iter().next())
{
Some(addr) => addr.to_string(),
None => {
warn!("Could not extract host from addresses");
warn!("No IP address for Chromecast service: {}", fullname);
return;
}
};
// Extract port from SRV record
let port = response
.records()
.find_map(|r| {
if let mdns::RecordKind::SRV { port, .. } = r.kind {
Some(port)
} else {
None
}
})
.unwrap_or(8009); // Default Chromecast port
let port = info.get_port();
// Extract TXT records for additional metadata
let txt_records: HashMap<String, String> = response
.records()
.filter_map(|r| {
if let mdns::RecordKind::TXT(ref data) = r.kind {
Some(data.clone())
} else {
None
}
})
.flat_map(|data| {
// data is Vec<String>, each string is "key=value"
data.into_iter().filter_map(|s| {
let parts: Vec<&str> = s.splitn(2, '=').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
})
.collect();
let uuid = info
.get_property_val_str("id")
.unwrap_or_default()
.to_string();
let uuid = if uuid.is_empty() {
format!("chromecast-{}-{}", host, port)
} else {
uuid
};
// Extract metadata from TXT records
let model = txt_records.get("md").cloned();
let uuid = txt_records
.get("id")
.cloned()
.unwrap_or_else(|| format!("chromecast-{}-{}", host, port));
let manufacturer = Some("Google Inc.".to_string());
let model = info.get_property_val_str("md").map(|s| s.to_string());
// Extract friendly name from TXT record "fn" if available
// Otherwise, extract from service instance name (PTR record)
let friendly_name = txt_records.get("fn").cloned().unwrap_or_else(|| {
// Fallback: extract from service name, removing the UUID suffix if present
service_name
.split("._googlecast._tcp.local")
.next()
.unwrap_or("Unknown Chromecast")
.split('-')
.take_while(|part| part.len() != 32) // Skip 32-char hex UUID
.collect::<Vec<_>>()
.join("-")
.trim()
.to_string()
});
let friendly_name = info
.get_property_val_str("fn")
.map(|s| s.to_string())
.unwrap_or_else(|| {
fullname
.split("._googlecast._tcp.local")
.next()
.unwrap_or("Unknown Chromecast")
.split('-')
.take_while(|part| part.len() != 32)
.collect::<Vec<_>>()
.join("-")
.trim()
.to_string()
});
debug!(
"Discovered Chromecast: {} at {}:{} (UUID: {}, Model: {:?})",
friendly_name, host, port, uuid, model
);
// Build UDN and check cache
let udn = format!("uuid:{}", uuid);
let default_max_age = 1800u64;
// Pour Chromecast, on utilise un max_age par défaut car mDNS n'a pas ce concept
let default_max_age = 1800u64; // 30 minutes
// Check cache to avoid redundant updates
if !UDNRegistry::should_fetch(self.udn_cache.clone(), &udn, default_max_age) {
debug!("Chromecast {} recently seen, skipping", udn);
return;
}
// Create RendererInfo for the registry
let renderer_info = build_renderer_info(
&uuid,
&friendly_name,
&host,
port,
model.as_deref(),
manufacturer.as_deref(),
Some("Google Inc."),
);
// Register the renderer
self.device_registry
.write()
.expect("DeviceRegistry mutex lock failed")