Add MusicBridge protocol documentation and implement system volume control
- Created PROTOCOL.md to outline the MusicBridge protocol v1, detailing setup, TLS, pairing, state management, and command structure. - Implemented SystemVolume class for managing system audio levels, including methods for setting, reading, changing, and muting volume. - Added unit tests for the MusicBridge agent, covering pairing, identity storage, command validation, and media state management. - Included tests for real-time media reading from Windows and ensured proper handling of artwork caching and retrieval.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
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
|
||||
{
|
||||
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 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();
|
||||
}
|
||||
Reference in New Issue
Block a user