⬆️ 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:
306
Blackboard/Todo/mdns_sd_migration.md
Normal file
306
Blackboard/Todo/mdns_sd_migration.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Migration `mdns` → `mdns-sd`
|
||||
|
||||
## Contexte
|
||||
|
||||
La crate `mdns 3.0.0` (dernière version, 2021, abandonnée) s'appuie sur `dns-parser 0.8.0`
|
||||
qui rejette les labels DNS non-ASCII avec `LabelIsNotAscii`. Les appareils Apple utilisent
|
||||
U+2019 (RIGHT SINGLE QUOTATION MARK) dans leurs noms (ex : "Sophie's MacBook Air"), ce qui
|
||||
spamme les logs en WARN à chaque paquet mDNS reçu.
|
||||
|
||||
`mdns-sd 0.19` (avril 2026, activement maintenu) gère l'UTF-8 correctement, fournit une API
|
||||
de plus haut niveau (service pré-assemblé), et ne nécessite pas `async-std`.
|
||||
|
||||
## Périmètre
|
||||
|
||||
Deux fichiers à modifier, un fichier à nettoyer :
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `pmocontrol/Cargo.toml` | Dépendances |
|
||||
| `pmocontrol/src/control_point.rs` | Thread de découverte mDNS (lignes 145–195) |
|
||||
| `pmocontrol/src/discovery/chromecast_discovery.rs` | Parsing des réponses mDNS |
|
||||
| `pmoserver/src/logs/mod.rs` | Filtre de bruit `mdns=error` devenu inutile |
|
||||
|
||||
---
|
||||
|
||||
## Étape 1 — `pmocontrol/Cargo.toml`
|
||||
|
||||
### Supprimer
|
||||
```toml
|
||||
mdns = "3.0"
|
||||
```
|
||||
|
||||
Vérifier si `async-std` et `futures-util` sont utilisés **ailleurs** que dans le thread mDNS.
|
||||
D'après l'analyse :
|
||||
- `async_std` : uniquement `control_point.rs:161` → **supprimer**
|
||||
- `futures-util` : uniquement `control_point.rs:150,167` → **supprimer**
|
||||
|
||||
### Ajouter
|
||||
```toml
|
||||
mdns-sd = "0.19"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 — `pmocontrol/src/control_point.rs`
|
||||
|
||||
### Code actuel (lignes 145–195) à remplacer intégralement
|
||||
|
||||
```rust
|
||||
// Thread de découverte mDNS pour Chromecast
|
||||
let registry_for_mdns = Arc::clone(®istry);
|
||||
let udn_cache_for_mdns = Arc::clone(&udn_cache);
|
||||
thread::spawn(move || {
|
||||
use crate::discovery::ChromecastDiscoveryManager;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
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";
|
||||
|
||||
async_std::task::block_on(async {
|
||||
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");
|
||||
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); }
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Nouveau code
|
||||
|
||||
```rust
|
||||
// Thread de découverte mDNS pour Chromecast
|
||||
let registry_for_mdns = Arc::clone(®istry);
|
||||
let udn_cache_for_mdns = Arc::clone(&udn_cache);
|
||||
thread::spawn(move || {
|
||||
use crate::discovery::ChromecastDiscoveryManager;
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
|
||||
let mut discovery_manager =
|
||||
ChromecastDiscoveryManager::new(registry_for_mdns, udn_cache_for_mdns);
|
||||
|
||||
debug!("Starting mDNS discovery thread for Chromecast devices");
|
||||
|
||||
// Note: mdns-sd requires the trailing dot in the service type
|
||||
const SERVICE_TYPE: &str = "_googlecast._tcp.local.";
|
||||
|
||||
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");
|
||||
// Pas de retrait actif du registre : le timeout habituel s'en charge
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("mDNS discovery receiver closed");
|
||||
});
|
||||
```
|
||||
|
||||
### Points d'attention
|
||||
- Le point final `.` dans `"_googlecast._tcp.local."` est **obligatoire** pour `mdns-sd`.
|
||||
- `receiver.recv()` est bloquant synchrone — pas besoin d'async runtime.
|
||||
- `ServiceDaemon` gère son propre thread interne ; inutile de relancer manuellement.
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 — `pmocontrol/src/discovery/chromecast_discovery.rs`
|
||||
|
||||
### Supprimer l'import `mdns`
|
||||
|
||||
```rust
|
||||
// Supprimer ces uses implicites via le type dans la signature
|
||||
use std::collections::HashMap; // <- plus nécessaire si on passe par TxtProperties
|
||||
```
|
||||
|
||||
### Ajouter l'import `mdns-sd`
|
||||
|
||||
```rust
|
||||
use mdns_sd::ServiceInfo;
|
||||
```
|
||||
|
||||
### Remplacer `handle_mdns_response` par `handle_service_resolved`
|
||||
|
||||
#### Code actuel (lignes 39–179) — ~80 lignes de parsing manuel
|
||||
|
||||
Toute la logique d'extraction PTR / A / AAAA / SRV / TXT disparaît.
|
||||
|
||||
#### Nouveau code
|
||||
|
||||
```rust
|
||||
/// Traite un service Chromecast résolu par mDNS-SD.
|
||||
///
|
||||
/// `ServiceInfo` arrive pré-assemblé : plus besoin de jointure manuelle
|
||||
/// des enregistrements PTR / A / SRV / TXT.
|
||||
pub fn handle_service_resolved(&mut self, info: &ServiceInfo) {
|
||||
let fullname = info.get_fullname().to_string();
|
||||
|
||||
debug!("Processing resolved Chromecast service: {}", fullname);
|
||||
|
||||
// Adresse IP : préférer IPv4
|
||||
let host = match info
|
||||
.get_addresses()
|
||||
.iter()
|
||||
.find(|a| a.is_ipv4())
|
||||
.or_else(|| info.get_addresses().iter().next())
|
||||
{
|
||||
Some(addr) => addr.to_string(),
|
||||
None => {
|
||||
warn!("No IP address for Chromecast service: {}", fullname);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let port = info.get_port();
|
||||
|
||||
// TXT records — API directe par clé
|
||||
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
|
||||
};
|
||||
|
||||
let model = info.get_property_val_str("md").map(|s| s.to_string());
|
||||
|
||||
let friendly_name = info
|
||||
.get_property_val_str("fn")
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
// Fallback : extraire depuis le fullname, supprimer le suffixe de service
|
||||
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
|
||||
);
|
||||
|
||||
let udn = format!("uuid:{}", uuid);
|
||||
let default_max_age = 1800u64;
|
||||
|
||||
if !UDNRegistry::should_fetch(self.udn_cache.clone(), &udn, default_max_age) {
|
||||
debug!("Chromecast {} recently seen, skipping", udn);
|
||||
return;
|
||||
}
|
||||
|
||||
let renderer_info = build_renderer_info(
|
||||
&uuid,
|
||||
&friendly_name,
|
||||
&host,
|
||||
port,
|
||||
model.as_deref(),
|
||||
Some("Google Inc."),
|
||||
);
|
||||
|
||||
self.device_registry
|
||||
.write()
|
||||
.expect("DeviceRegistry mutex lock failed")
|
||||
.push_renderer(&renderer_info, default_max_age as u32);
|
||||
}
|
||||
```
|
||||
|
||||
### Supprimer
|
||||
- L'import `use std::collections::HashMap` (plus utilisé)
|
||||
- Tout le bloc `handle_mdns_response` (lignes 39–179)
|
||||
|
||||
### Conserver sans modification
|
||||
- `build_renderer_info` (lignes 182–234)
|
||||
- `extract_host_from_location` / `extract_port_from_location` (lignes 239–258)
|
||||
- Les tests (lignes 260–287)
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 — `pmoserver/src/logs/mod.rs`
|
||||
|
||||
Le filtre de bruit `mdns=error` injecté dans `build_filter_with_noise_suppressions` n'est
|
||||
plus nécessaire. Deux options :
|
||||
|
||||
**Option A (recommandée)** — Supprimer l'entrée du tableau :
|
||||
```rust
|
||||
const NOISE_FILTERS: &[(&str, &str)] = &[
|
||||
// ("mdns", "mdns=error"), // supprimé : migration vers mdns-sd
|
||||
];
|
||||
```
|
||||
Ou supprimer `build_filter_with_noise_suppressions` entièrement si aucun autre bruit n'est
|
||||
à filtrer, et revenir à `EnvFilter::try_new(base)` direct.
|
||||
|
||||
**Option B** — Laisser en place. La directive `mdns=error` ne cause aucun dommage si la
|
||||
crate `mdns` n'est plus dans le build (elle sera simplement ignorée).
|
||||
|
||||
---
|
||||
|
||||
## Résumé des diffs
|
||||
|
||||
| Fichier | Lignes supprimées | Lignes ajoutées |
|
||||
|---|---|---|
|
||||
| `Cargo.toml` | `mdns`, `async-std`, `futures-util` | `mdns-sd` |
|
||||
| `control_point.rs` | ~50 (async block) | ~35 (sync recv loop) |
|
||||
| `chromecast_discovery.rs` | ~80 (parsing manuel) | ~50 (lecture ServiceInfo) |
|
||||
| `logs/mod.rs` | ~5 (filtre bruit) | 0 |
|
||||
|
||||
## Vérification
|
||||
|
||||
Après implémentation :
|
||||
1. `cargo check -p pmocontrol` sans erreurs ni `use of undeclared crate mdns`
|
||||
2. `cargo check -p pmoserver` sans erreurs
|
||||
3. Tester la découverte d'un Chromecast en réseau local
|
||||
4. Vérifier l'absence de `LabelIsNotAscii` dans les logs avec des appareils Apple présents
|
||||
Reference in New Issue
Block a user