Files
pmomusic/Blackboard/Todo/mdns_sd_migration.md
Eric Coissac 934bbaf4b8 ⬆️ 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
2026-04-09 15:50:18 +02:00

9.4 KiB
Raw Blame History

Migration mdnsmdns-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 145195)
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

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:161supprimer
  • futures-util : uniquement control_point.rs:150,167supprimer

Ajouter

mdns-sd = "0.19"

Étape 2 — pmocontrol/src/control_point.rs

Code actuel (lignes 145195) à remplacer intégralement

// Thread de découverte mDNS pour Chromecast
let registry_for_mdns = Arc::clone(&registry);
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

// Thread de découverte mDNS pour Chromecast
let registry_for_mdns = Arc::clone(&registry);
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

// 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

use mdns_sd::ServiceInfo;

Remplacer handle_mdns_response par handle_service_resolved

Code actuel (lignes 39179) — ~80 lignes de parsing manuel

Toute la logique d'extraction PTR / A / AAAA / SRV / TXT disparaît.

Nouveau code

/// 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 39179)

Conserver sans modification

  • build_renderer_info (lignes 182234)
  • extract_host_from_location / extract_port_from_location (lignes 239258)
  • Les tests (lignes 260287)

É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 :

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