using System.Net; using Makaretu.Dns; namespace MusicBridge.Agent.Network; // All lifecycle calls run on the console loop, not on network-change callback threads. internal sealed class MdnsPublisher(string fingerprint, int port) : IDisposable { private MulticastService? multicast; private ServiceDiscovery? discovery; private ServiceProfile? profile; private string addressesKey = ""; private DateTimeOffset nextAnnouncement; private DateTimeOffset lastRefresh; public string Status { get; private set; } = "mDNS: ожидание локальной сети."; public static ServiceProfile CreateProfile(string fingerprint, int port, IPAddress[] addresses) { if (port is < 1 or > 65535 || addresses.Length == 0 || addresses.Any(a => !LocalAddresses.IsEligible(a))) throw new ArgumentException("Неверный порт или адрес объявления mDNS."); var descriptor = DiscoveryDescriptor.Create(fingerprint); var result = new ServiceProfile(descriptor.InstanceName, "_musicbridge._tcp", (ushort)port, addresses); foreach (var pair in descriptor.TxtRecords()) result.AddProperty(pair.Key, pair.Value); foreach (var record in result.Resources) record.TTL = TimeSpan.FromSeconds(120); return result; } public void Refresh(IPAddress[] addresses) { try { // Rebind after a long pause (sleep/resume), even if Windows kept the same IP. var now = DateTimeOffset.UtcNow; if (lastRefresh != default && (now - lastRefresh > TimeSpan.FromSeconds(20) || now < lastRefresh)) Stop(); lastRefresh = now; var key = string.Join(",", addresses.Select(a => a.ToString())); if (discovery is null || key != addressesKey) { Stop(); if (addresses.Length == 0) { Status = "mDNS: нет адреса домашней сети."; return; } multicast = new MulticastService(nics => nics.Where(nic => nic.GetIPProperties().UnicastAddresses.Any(a => addresses.Contains(a.Address)))) { UseIpv4 = true, UseIpv6 = false }; profile = CreateProfile(fingerprint, port, addresses); discovery = new ServiceDiscovery(multicast); discovery.Advertise(profile); multicast.Start(); addressesKey = key; nextAnnouncement = DateTimeOffset.MinValue; } if (DateTimeOffset.UtcNow >= nextAnnouncement) { var message = new Makaretu.Dns.Message { QR = true, AA = true }; message.Answers.Add(new PTRRecord { Name = profile!.QualifiedServiceName, DomainName = profile.FullyQualifiedName, TTL = TimeSpan.FromSeconds(120) }); foreach (var record in profile.Resources) message.Answers.Add(record); multicast!.SendAnswer(message, checkDuplicate: false); nextAnnouncement = DateTimeOffset.UtcNow.AddSeconds(30); } Status = $"mDNS: объявляется {profile!.FullyQualifiedName}"; } catch (Exception ex) { Stop(); Status = $"mDNS недоступен: {ex.Message}"; } } private void Stop() { try { if (profile is not null) discovery?.Unadvertise(profile); } catch (Exception) { /* Best-effort goodbye when a network has disappeared. */ } discovery?.Dispose(); multicast?.Dispose(); discovery = null; multicast = null; profile = null; addressesKey = ""; } public void Dispose() => Stop(); }