using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; namespace MusicCast.Net.Client; public partial class MusicCastDiscoveryService { private static readonly byte[] MulticastPacket = """ M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" ST: urn:schemas-upnp-org:device:MediaRenderer:1 MX: 1 """u8.ToArray(); private static readonly HttpClient _httpClient = new(); private const string LocationUrlGroupName = "url"; [GeneratedRegex(@$"Location:\s(?<{LocationUrlGroupName}>http[s]?:\/\/.+\.xml)", RegexOptions.IgnoreCase)] private static partial Regex LocationRegex(); /// /// Get the ip address associated with the current local network. /// If multiple networks cards are installed it only returns the first one. /// /// public IPAddress? GetLocalSubnetAddress() { // Get a list of all network interfaces (usually one per network card, dialup, and VPN connection) var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); foreach (NetworkInterface network in networkInterfaces) { // Read the IP configuration for each network IPInterfaceProperties properties = network.GetIPProperties(); if (network.NetworkInterfaceType is NetworkInterfaceType.Ethernet or NetworkInterfaceType.Wireless80211 && network.OperationalStatus == OperationalStatus.Up && !network.Description.Contains("virtual", StringComparison.OrdinalIgnoreCase) && !network.Description.Contains("pseudo", StringComparison.OrdinalIgnoreCase)) { // Each network interface may have multiple IP addresses foreach (UnicastIPAddressInformation address in properties.UnicastAddresses) { // We're only interested in IPv4 addresses for now if (address.Address.AddressFamily != AddressFamily.InterNetwork) continue; // Ignore loopback addresses (e.g., 127.0.0.1) if (IPAddress.IsLoopback(address.Address)) continue; return address.Address; } } } return null; } public Task> GetMusicCastDeviceAddresses(IPAddress bindAddress) => GetMusicCastDeviceAddresses(bindAddress, TimeSpan.FromSeconds(2)); /// /// Scan for MusicCast devices on the network associated with the bind address. Only return devices that support /// the YamahaExtendedControl v1 api /// /// /// Duration of the scan /// public async Task> GetMusicCastDeviceAddresses(IPAddress bindAddress, TimeSpan scanDuration) { using var client = new UdpClient(AddressFamily.InterNetwork); client.Client.Bind(new IPEndPoint(bindAddress, 0)); client.JoinMulticastGroup(IPAddress.Parse("239.255.255.250")); await client.SendAsync(MulticastPacket, "239.255.255.250", 1900); await client.SendAsync(MulticastPacket, "239.255.255.250", 1900); await client.SendAsync(MulticastPacket, "239.255.255.250", 1900); var musicCastDevices = new HashSet(); // Start scanning var cancellationToken = new CancellationTokenSource(scanDuration).Token; try { while (!cancellationToken.IsCancellationRequested) { var received = await client.ReceiveAsync(cancellationToken); var message = Encoding.UTF8.GetString(received.Buffer); // Try to find the location of the DLNA description file var match = LocationRegex().Match(message); if (!match.Success || !match.Groups.TryGetValue(LocationUrlGroupName, out var group)) { continue; } var location = group.Value; var apiUrl = await GetApiUrlFromDlnaDescriptionFile(location, cancellationToken); if (apiUrl is not null) { musicCastDevices.Add(apiUrl); } } } catch (OperationCanceledException) { // Normal flow after scan duration } return musicCastDevices; } private static async Task GetApiUrlFromDlnaDescriptionFile(string location, CancellationToken cancellationToken) { try { var response = await _httpClient.GetAsync(location, cancellationToken); await using var descriptionFileStream = await response.Content.ReadAsStreamAsync(cancellationToken); var document = await XDocument.LoadAsync(descriptionFileStream, LoadOptions.None, cancellationToken); XNamespace yamahaNamespace = "urn:schemas-yamaha-com:device-1-0"; var urlBaseNode = document.Descendants(yamahaNamespace + "X_URLBase").FirstOrDefault(); if (urlBaseNode is null) { return null; } var yamahaControlUrlNodes = document.Descendants(yamahaNamespace + "X_yxcControlURL"); foreach (var yamahaControlUrlNode in yamahaControlUrlNodes) { if (yamahaControlUrlNode is { Value: "/YamahaExtendedControl/v1/" }) { return urlBaseNode.Value; } } } catch (Exception) { // ignored } return null; } }