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,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);
|
||||
}
|
||||
Reference in New Issue
Block a user