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:
2026-09-09 01:05:52 +03:00
parent 5e476c4c2f
commit d5e827112a
23 changed files with 2067 additions and 72 deletions
+58
View File
@@ -0,0 +1,58 @@
namespace MusicBridge.Agent.Media;
internal sealed record MediaCommand(string Type, double? Value = null, string? Id = null);
internal sealed record CommandResult(bool Success, string Code, string Message, string? Id = null);
internal sealed record PlayerCapabilities(bool Play = false, bool Pause = false,
bool Toggle = false, bool Next = false, bool Previous = false, bool Seek = false);
internal sealed record MediaState
{
public int ProtocolVersion { get; init; } = 1;
public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow;
public bool HasSession { get; init; }
public string Title { get; init; } = "";
public string Artist { get; init; } = "";
public string Source { get; init; } = "";
public string PlaybackStatus { get; init; } = "Closed";
public double PositionSeconds { get; init; }
public double StartSeconds { get; init; }
public double EndSeconds { get; init; }
public double MinSeekSeconds { get; init; }
public double MaxSeekSeconds { get; init; }
public double PlaybackRate { get; init; } = 1;
public PlayerCapabilities Capabilities { get; init; } = new();
public float? Volume { get; init; }
public bool? Muted { get; init; }
public string? MediaError { get; init; }
public string? VolumeError { get; init; }
public string? ArtworkId { get; init; }
public string? ArtworkError { get; init; }
}
internal interface IMediaService
{
event Action? Changed;
Artwork? GetArtwork(string id);
Task<MediaState> ReadAsync(CancellationToken cancellationToken);
Task<CommandResult> ExecuteAsync(MediaCommand command, CancellationToken cancellationToken);
}
internal static class CommandValidation
{
public static CommandResult? Validate(MediaCommand command)
{
if (command.Id?.Length > 64) return Error("invalid_command", "Id длиннее 64 символов.");
var valid = command.Type switch
{
"play" or "pause" or "toggle" or "next" or "previous" or "toggleMute" => command.Value is null,
"seek" => command.Value is double s && double.IsFinite(s) && s >= 0 && s <= TimeSpan.MaxValue.TotalSeconds / 2,
"seekRelative" => command.Value is double d && double.IsFinite(d) && Math.Abs(d) <= 3600,
"volume" => command.Value is double v && double.IsFinite(v) && v >= 0 && v <= 1,
"volumeRelative" => command.Value is double d && double.IsFinite(d) && Math.Abs(d) <= 1,
_ => false
};
return valid ? null : Error("invalid_command", "Неизвестная команда или недопустимое значение.");
}
public static CommandResult Error(string code, string message) => new(false, code, message);
}