use std::net::{IpAddr, SocketAddr};
use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo};
use super::protocol::VERSION;
const SERVICE: &str = "_paintparty._tcp.local.";
#[derive(Clone, Debug, PartialEq)]
pub struct Nearby {
fullname: String,
pub name: String,
pub kids: usize,
pub addr: SocketAddr,
pub compatible: bool,
}
pub struct Discovery {
daemon: ServiceDaemon,
browse: Option<mdns_sd::Receiver<ServiceEvent>>,
pub nearby: Vec<Nearby>,
advertised: Option<String>,
instance: String,
}
impl Discovery {
pub fn new() -> Option<Self> {
let daemon = ServiceDaemon::new().ok()?;
let seed = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos() as u64);
let instance = format!("paint-{:08x}", (seed ^ (std::process::id() as u64) << 20) as u32);
Some(Self { daemon, browse: None, nearby: Vec::new(), advertised: None, instance })
}
pub fn browse(&mut self) {
if self.browse.is_none() {
self.browse = self.daemon.browse(SERVICE).ok();
}
}
pub fn poll(&mut self) -> bool {
let Some(rx) = &self.browse else { return false };
let mut changed = false;
while let Ok(event) = rx.try_recv() {
match event {
ServiceEvent::ServiceResolved(info) => {
if Some(&info.fullname) == self.advertised.as_ref() {
continue;
}
let Some(ip) = info.get_addresses_v4().into_iter().min() else {
continue;
};
let found = Nearby {
fullname: info.fullname.clone(),
name: info.get_property_val_str("name").unwrap_or("A friend").chars().take(24).collect(),
kids: info.get_property_val_str("kids").and_then(|k| k.parse().ok()).unwrap_or(1),
addr: SocketAddr::new(IpAddr::V4(ip), info.get_port()),
compatible: info.get_property_val_str("v") == Some(&VERSION.to_string()),
};
match self.nearby.iter_mut().find(|n| n.fullname == found.fullname) {
Some(n) if *n == found => {}
Some(n) => {
*n = found;
changed = true;
}
None => {
self.nearby.push(found);
changed = true;
}
}
}
ServiceEvent::ServiceRemoved(_, fullname) => {
let before = self.nearby.len();
self.nearby.retain(|n| n.fullname != fullname);
changed |= self.nearby.len() != before;
}
_ => {}
}
}
changed
}
pub fn advertise(&mut self, name: &str, kids: usize, port: u16) {
let host = format!("{}.local.", self.instance);
let props = [("name", name.to_owned()), ("kids", kids.to_string()), ("v", VERSION.to_string())];
let Ok(info) = ServiceInfo::new(SERVICE, &self.instance, &host, (), port, &props[..]) else {
return;
};
let info = info.enable_addr_auto();
self.advertised = Some(info.get_fullname().to_owned());
self.nearby.retain(|n| Some(&n.fullname) != self.advertised.as_ref());
let _ = self.daemon.register(info);
}
pub fn stop_advertising(&mut self) {
if let Some(fullname) = self.advertised.take() {
let _ = self.daemon.unregister(&fullname);
}
}
}
impl Drop for Discovery {
fn drop(&mut self) {
self.stop_advertising();
let _ = self.daemon.shutdown();
}
}